382 lines
12 KiB
HTML
382 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Network Connection Graph - Iteration 2</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
#viz-container {
|
|
max-width: 1000px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 15px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
padding: 30px;
|
|
}
|
|
|
|
h1 {
|
|
color: #2d3748;
|
|
margin-bottom: 10px;
|
|
font-size: 2em;
|
|
}
|
|
|
|
.subtitle {
|
|
color: #718096;
|
|
margin-bottom: 30px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.controls {
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
gap: 15px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.controls button {
|
|
padding: 10px 20px;
|
|
background: #1e3c72;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: background 0.3s;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.controls button:hover {
|
|
background: #2a5298;
|
|
}
|
|
|
|
.chart-container {
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.node {
|
|
cursor: pointer;
|
|
stroke: #fff;
|
|
stroke-width: 2px;
|
|
}
|
|
|
|
.node:hover {
|
|
stroke: #ff6b6b;
|
|
stroke-width: 3px;
|
|
}
|
|
|
|
.link {
|
|
stroke: #999;
|
|
stroke-opacity: 0.6;
|
|
}
|
|
|
|
.node-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
fill: #2d3748;
|
|
text-anchor: middle;
|
|
pointer-events: none;
|
|
user-select: none;
|
|
}
|
|
|
|
.tooltip {
|
|
position: absolute;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
color: white;
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
font-size: 13px;
|
|
z-index: 1000;
|
|
max-width: 200px;
|
|
}
|
|
|
|
.attribution {
|
|
margin-top: 30px;
|
|
padding-top: 20px;
|
|
border-top: 1px solid #e2e8f0;
|
|
font-size: 0.85em;
|
|
color: #718096;
|
|
}
|
|
|
|
.attribution a {
|
|
color: #1e3c72;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.attribution a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
#viz-container {
|
|
padding: 20px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
.controls {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="viz-container">
|
|
<h1>Open Source Project Dependency Network</h1>
|
|
<p class="subtitle">Force-directed graph showing package relationships</p>
|
|
|
|
<div class="controls">
|
|
<button id="restart-btn">Reset Simulation</button>
|
|
<button id="toggle-labels-btn">Toggle Labels</button>
|
|
</div>
|
|
|
|
<div class="chart-container"></div>
|
|
|
|
<div class="attribution">
|
|
<p><strong>Visualization inspired by:</strong> <a href="https://observablehq.com/@d3/force-directed-graph" target="_blank">D3 Force-Directed Graph</a></p>
|
|
<p><strong>Data from:</strong> GitHub API (simulated)</p>
|
|
<p><strong>Created:</strong> 2025-10-10T23:51:20Z</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Metadata section (required for state tracking) -->
|
|
<div id="metadata" style="display:none;">
|
|
{
|
|
"iteration": 2,
|
|
"web_source": "https://observablehq.com/@d3/force-directed-graph",
|
|
"techniques_learned": [
|
|
"Force-directed layout with d3.forceSimulation",
|
|
"Dynamic node dragging with pointer events",
|
|
"Link strength and distance constraints"
|
|
],
|
|
"data_source": "https://api.github.com/repos",
|
|
"created": "2025-10-10T23:51:20Z"
|
|
}
|
|
</div>
|
|
|
|
<div class="tooltip"></div>
|
|
|
|
<!-- External libraries -->
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
|
|
<script>
|
|
/**
|
|
* Open Source Project Dependency Network - Iteration 2
|
|
*
|
|
* Web Source: https://observablehq.com/@d3/force-directed-graph
|
|
* Techniques Learned:
|
|
* 1. Force-directed layout with d3.forceSimulation
|
|
* 2. Dynamic node dragging with pointer events
|
|
* 3. Link strength and distance constraints
|
|
*
|
|
* Data Source: GitHub API (simulated)
|
|
*
|
|
* Created: 2025-10-10T23:51:20Z
|
|
* Run ID: test_run_001
|
|
*/
|
|
|
|
// Simulated network data
|
|
const nodes = [
|
|
{ id: 'react', group: 1, size: 30 },
|
|
{ id: 'vue', group: 1, size: 25 },
|
|
{ id: 'angular', group: 1, size: 28 },
|
|
{ id: 'webpack', group: 2, size: 20 },
|
|
{ id: 'babel', group: 2, size: 18 },
|
|
{ id: 'typescript', group: 2, size: 22 },
|
|
{ id: 'eslint', group: 3, size: 15 },
|
|
{ id: 'jest', group: 3, size: 16 },
|
|
{ id: 'redux', group: 4, size: 14 },
|
|
{ id: 'mobx', group: 4, size: 12 },
|
|
{ id: 'd3', group: 5, size: 20 },
|
|
{ id: 'three.js', group: 5, size: 18 }
|
|
];
|
|
|
|
const links = [
|
|
{ source: 'react', target: 'webpack', value: 3 },
|
|
{ source: 'react', target: 'babel', value: 3 },
|
|
{ source: 'react', target: 'redux', value: 2 },
|
|
{ source: 'vue', target: 'webpack', value: 2 },
|
|
{ source: 'vue', target: 'babel', value: 2 },
|
|
{ source: 'angular', target: 'typescript', value: 3 },
|
|
{ source: 'angular', target: 'webpack', value: 2 },
|
|
{ source: 'react', target: 'jest', value: 2 },
|
|
{ source: 'vue', target: 'jest', value: 2 },
|
|
{ source: 'react', target: 'eslint', value: 1 },
|
|
{ source: 'vue', target: 'eslint', value: 1 },
|
|
{ source: 'angular', target: 'eslint', value: 1 },
|
|
{ source: 'redux', target: 'typescript', value: 1 },
|
|
{ source: 'mobx', target: 'typescript', value: 1 }
|
|
];
|
|
|
|
// Set up dimensions
|
|
const width = Math.min(900, window.innerWidth - 100);
|
|
const height = 600;
|
|
|
|
// Color scale for groups
|
|
const color = d3.scaleOrdinal(d3.schemeCategory10);
|
|
|
|
// Create SVG
|
|
const svg = d3.select('.chart-container')
|
|
.append('svg')
|
|
.attr('width', width)
|
|
.attr('height', height)
|
|
.attr('viewBox', [0, 0, width, height])
|
|
.attr('role', 'img')
|
|
.attr('aria-label', 'Network graph showing package dependencies');
|
|
|
|
// Get tooltip element
|
|
const tooltip = d3.select('.tooltip');
|
|
|
|
// Learned Technique 1: Force-directed layout with d3.forceSimulation
|
|
const simulation = d3.forceSimulation(nodes)
|
|
.force('link', d3.forceLink(links)
|
|
.id(d => d.id)
|
|
.distance(d => 100 / d.value)) // Learned Technique 3: Link distance constraints
|
|
.force('charge', d3.forceManyBody().strength(-300))
|
|
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
.force('collision', d3.forceCollide().radius(d => d.size + 5));
|
|
|
|
// Create links
|
|
const link = svg.append('g')
|
|
.selectAll('line')
|
|
.data(links)
|
|
.join('line')
|
|
.attr('class', 'link')
|
|
.attr('stroke-width', d => Math.sqrt(d.value) * 2);
|
|
|
|
// Create nodes
|
|
const node = svg.append('g')
|
|
.selectAll('circle')
|
|
.data(nodes)
|
|
.join('circle')
|
|
.attr('class', 'node')
|
|
.attr('r', d => d.size)
|
|
.attr('fill', d => color(d.group))
|
|
.attr('role', 'graphics-symbol')
|
|
.attr('aria-label', d => `Package: ${d.id}`)
|
|
.call(drag(simulation)); // Learned Technique 2: Dynamic node dragging
|
|
|
|
// Add labels
|
|
const labels = svg.append('g')
|
|
.selectAll('text')
|
|
.data(nodes)
|
|
.join('text')
|
|
.attr('class', 'node-label')
|
|
.text(d => d.id);
|
|
|
|
// Tooltip interactions
|
|
node.on('mouseover', function(event, d) {
|
|
const connections = links.filter(l =>
|
|
l.source.id === d.id || l.target.id === d.id
|
|
);
|
|
|
|
tooltip
|
|
.style('opacity', 1)
|
|
.html(`
|
|
<strong>${d.id}</strong><br>
|
|
Group: ${d.group}<br>
|
|
Connections: ${connections.length}
|
|
`)
|
|
.style('left', (event.pageX + 10) + 'px')
|
|
.style('top', (event.pageY - 28) + 'px');
|
|
})
|
|
.on('mouseout', function() {
|
|
tooltip.style('opacity', 0);
|
|
});
|
|
|
|
// Update positions on 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('cx', d => d.x)
|
|
.attr('cy', d => d.y);
|
|
|
|
labels
|
|
.attr('x', d => d.x)
|
|
.attr('y', d => d.y + d.size + 15);
|
|
});
|
|
|
|
// Learned Technique 2: Drag behavior for interactive repositioning
|
|
function drag(simulation) {
|
|
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;
|
|
}
|
|
|
|
return d3.drag()
|
|
.on('start', dragstarted)
|
|
.on('drag', dragged)
|
|
.on('end', dragended);
|
|
}
|
|
|
|
// Control buttons
|
|
let labelsVisible = true;
|
|
|
|
document.getElementById('restart-btn').addEventListener('click', () => {
|
|
simulation.alpha(1).restart();
|
|
});
|
|
|
|
document.getElementById('toggle-labels-btn').addEventListener('click', () => {
|
|
labelsVisible = !labelsVisible;
|
|
labels.style('opacity', labelsVisible ? 1 : 0);
|
|
});
|
|
|
|
// Keyboard accessibility
|
|
node.attr('tabindex', 0)
|
|
.on('focus', function(event, d) {
|
|
const connections = links.filter(l =>
|
|
l.source.id === d.id || l.target.id === d.id
|
|
);
|
|
|
|
tooltip
|
|
.style('opacity', 1)
|
|
.html(`
|
|
<strong>${d.id}</strong><br>
|
|
Group: ${d.group}<br>
|
|
Connections: ${connections.length}
|
|
`)
|
|
.style('left', (event.pageX + 10) + 'px')
|
|
.style('top', (event.pageY - 28) + 'px');
|
|
})
|
|
.on('blur', function() {
|
|
tooltip.style('opacity', 0);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|