<!DOCTYPE html>
<html>
<head>
<title>Component Dependency Graph</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: 0.6;
stroke-width: 1px;
}
.node text {
font-size: 12px;
font-family: Arial, sans-serif;
}
.tooltip {
position: absolute;
padding: 8px;
background: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 4px;
font-size: 12px;
pointer-events: none;
}
#graph {
width: 100%;
height: 100vh;
overflow: hidden;
}
svg {
width: 100%;
height: 100%;
}
.search-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
background: white;
padding: 10px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.search-input {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
width: 200px;
font-size: 14px;
}
.export-container {
position: fixed;
top: 20px;
left: 20px;
z-index: 1000;
background: white;
padding: 10px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: flex;
gap: 8px;
}
.export-button {
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.export-button:hover {
background: #f0f0f0;
}
</style>
</head>
<body>
<div class="search-container">
<input type="text"
class="search-input"
placeholder="Search components..."
oninput="filterNodes(this.value)">
</div>
<div class="export-container">
<button class="export-button" onclick="exportAsSVG()">Export SVG</button>
<button class="export-button" onclick="exportAsPNG()">Export PNG</button>
</div>
<div id="graph"></div>
<script>
const graphData = {"nodes":[{"name":"ComponentA","file_path":"/path/to/ComponentA.tsx","prop_usage":{}},{"name":"ComponentB","file_path":"/path/to/ComponentB.tsx","prop_usage":{}},{"name":"ComponentC","file_path":"/path/to/ComponentC.tsx","prop_usage":{}},{"name":"ComponentD","file_path":"/path/to/ComponentD.tsx","prop_usage":{}},{"name":"ComponentE","file_path":"/path/to/ComponentE.tsx","prop_usage":{}}],"edges":[[0,1],[2,1],[3,1],[3,4]]};
const width = window.innerWidth;
const height = window.innerHeight;
const tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
const svg = d3.select("#graph")
.append("svg")
.attr("width", width)
.attr("height", height);
const g = svg.append("g");
const zoom = d3.zoom().scaleExtent([0.5, 4]).on("zoom", function(event) {
g.attr("transform", event.transform);
});
svg.call(zoom);
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(100))
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(50));
const incomingEdges = {};
graphData.edges.forEach(edge => {
const targetId = edge[1];
incomingEdges[targetId] = (incomingEdges[targetId] || 0) + 1;
});
renderGraph(graphData);
function renderGraph(data) {
debugger
const links = data.edges.map(edge => ({
source: edge[0],
target: edge[1]
}));
const link = g.append("g")
.selectAll("line")
.data(links)
.enter()
.append("line")
.attr("class", "link")
.attr("marker-end", "url(#arrowhead)");
const node = g.append("g")
.selectAll("g")
.data(data.nodes)
.enter()
.append("g")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("mouseover", function(event, d) {
const transform = d3.zoomTransform(svg.node());
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(`Path: ${d.file_path}<br/>Props: ${Object.entries(d.prop_usage).map(([prop, count]) => `${prop}(${count})`).join(', ')}`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
node.append("circle")
.attr("class", "node")
.attr("r", getNodeRadius)
.attr("fill", getNodeColor);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(d => d.name);
function ticked() {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => {
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
const length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) return d.target.x;
const targetRadius = getNodeRadius(d.target, d.target.index);
return d.target.x - (dx * (targetRadius + 5)) / length; })
.attr("y2", d => {
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
const length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) return d.target.y;
const targetRadius = getNodeRadius(d.target, d.target.index);
return d.target.y - (dy * (targetRadius + 5)) / length; });
node
.attr("transform", d => `translate(${d.x},${d.y})`);
}
simulation
.nodes(data.nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
}
function getNodeRadius(d, i) {
const baseRadius = 5;
const edgeCount = incomingEdges[i] || 0;
return baseRadius + (edgeCount * 2); }
svg.append("defs").append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 5) .attr("refY", 0)
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", "#999");
function getNodeColor(node) {
const edgeCount = incomingEdges[node.index] || 0;
if (edgeCount === 0) return "#69b3a2";
if (edgeCount < 3) return "#3498db";
if (edgeCount < 5) return "#e67e22";
return "#e74c3c";
}
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
}
function filterNodes(searchTerm) {
searchTerm = searchTerm.toLowerCase();
const filteredNodes = searchTerm === ""
? graphData.nodes
: graphData.nodes.filter(d => d.name.toLowerCase().includes(searchTerm));
const filteredIndices = new Set(filteredNodes.map((_, i) => i));
const filteredEdges = searchTerm === ""
? graphData.edges
: graphData.edges.filter(d =>
filteredIndices.has(d[0]) && filteredIndices.has(d[1]));
g.selectAll("*").remove();
const filteredData = {
nodes: filteredNodes,
edges: filteredEdges
};
renderGraph(filteredData);
}
function showTooltip(event, d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
const edgeCount = incomingEdges[d.index] || 0;
tooltip.html(`Path: ${d.file_path}<br/>Props: ${Object.entries(d.prop_usage).map(([prop, count]) => `${prop}(${count})`).join(', ')}<br/>Incoming Edges: ${edgeCount}`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
}
function hideTooltip() {
tooltip.transition()
.duration(500)
.style("opacity", 0);
}
document.addEventListener('keydown', function(e) {
if (e.key === '/' && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
document.querySelector('.search-input').focus();
}
if (e.key === 'Escape') {
document.querySelector('.search-input').value = '';
filterNodes('');
}
});
function exportAsSVG() {
const svgElement = document.querySelector('svg');
const clonedSvg = svgElement.cloneNode(true);
const styleElement = document.createElement('style');
styleElement.textContent = `
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: 0.6;
stroke-width: 1px;
}
.node text {
font-size: 12px;
font-family: Arial, sans-serif;
}
marker#arrowhead path {
fill: #999;
}
`;
clonedSvg.insertBefore(styleElement, clonedSvg.firstChild);
const bbox = svgElement.getBBox();
clonedSvg.setAttribute('width', bbox.width + bbox.x * 2);
clonedSvg.setAttribute('height', bbox.height + bbox.y * 2);
clonedSvg.setAttribute('viewBox', `${bbox.x} ${bbox.y} ${bbox.width + bbox.x} ${bbox.height + bbox.y}`);
const serializer = new XMLSerializer();
const source = serializer.serializeToString(clonedSvg);
const svgData = '<?xml version="1.0" standalone="no"?>\r\n' + source;
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
const svgUrl = URL.createObjectURL(svgBlob);
const downloadLink = document.createElement('a');
downloadLink.href = svgUrl;
downloadLink.download = 'component-graph.svg';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
URL.revokeObjectURL(svgUrl);
}
function exportAsPNG() {
const svgElement = document.querySelector('svg');
const bbox = svgElement.getBBox();
const width = bbox.width + bbox.x * 2;
const height = bbox.height + bbox.y * 2;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
const clonedSvg = svgElement.cloneNode(true);
const styleElement = document.createElement('style');
styleElement.textContent = `
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: 0.6;
stroke-width: 1px;
}
.node text {
font-size: 12px;
font-family: Arial, sans-serif;
}
marker#arrowhead path {
fill: #999;
}
`;
clonedSvg.insertBefore(styleElement, clonedSvg.firstChild);
clonedSvg.setAttribute('width', width);
clonedSvg.setAttribute('height', height);
clonedSvg.setAttribute('viewBox', `${bbox.x} ${bbox.y} ${bbox.width + bbox.x} ${bbox.height + bbox.y}`);
const image = new Image();
const svgData = new XMLSerializer().serializeToString(clonedSvg);
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
const svgUrl = URL.createObjectURL(svgBlob);
image.onload = function() {
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height);
ctx.drawImage(image, 0, 0);
const pngUrl = canvas.toDataURL('image/png');
const downloadLink = document.createElement('a');
downloadLink.href = pngUrl;
downloadLink.download = 'component-graph.png';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
URL.revokeObjectURL(svgUrl);
};
image.src = svgUrl;
}
</script>
</body>
</html>