diff --git a/src/aggregator/web/index.html b/src/aggregator/web/index.html
index 7b03150..b22ed38 100644
--- a/src/aggregator/web/index.html
+++ b/src/aggregator/web/index.html
@@ -594,10 +594,10 @@
-
Done
-
In Progress
-
To Do
-
Border = Project
+
Done
+
In Progress
+
To Do
+
Border = Project
Drag nodes to reposition • Double-click to unpin • Scroll to zoom • Click node for details
@@ -1369,7 +1369,7 @@
const container = document.getElementById('dependency-graph');
container.innerHTML = '';
- // Filter tasks
+ // Filter tasks - show ALL tasks, not just those with dependencies
let filteredTasks = selectedProject
? tasks.filter(t => t.projectName === selectedProject)
: tasks;
@@ -1381,32 +1381,47 @@
});
}
- // Get tasks with dependencies
- const taskIds = new Set(filteredTasks.map(t => t.id));
- const tasksWithDeps = filteredTasks.filter(t =>
- (t.dependencies && t.dependencies.length > 0) ||
- filteredTasks.some(other => other.dependencies && other.dependencies.includes(t.id))
- );
-
- if (tasksWithDeps.length === 0) {
- container.innerHTML = 'No tasks with dependencies to display.
';
+ if (filteredTasks.length === 0) {
+ container.innerHTML = 'No tasks to display.
';
return;
}
- // Build nodes and links
- const nodes = tasksWithDeps.map(t => ({
- id: t.id,
- title: t.title,
- status: t.status,
- projectName: t.projectName,
- projectColor: t.projectColor,
- projectPath: t.projectPath,
- priority: t.priority
- }));
+ // Group tasks by project
+ const projectGroups = new Map();
+ filteredTasks.forEach(t => {
+ if (!projectGroups.has(t.projectName)) {
+ projectGroups.set(t.projectName, {
+ name: t.projectName,
+ color: t.projectColor,
+ tasks: []
+ });
+ }
+ projectGroups.get(t.projectName).tasks.push(t);
+ });
+ // Create cluster centers for each project
+ const projectList = Array.from(projectGroups.values());
+ const numProjects = projectList.length;
+
+ // Build nodes with cluster assignment
+ const nodes = filteredTasks.map(t => {
+ const projectIndex = projectList.findIndex(p => p.name === t.projectName);
+ return {
+ id: t.id,
+ title: t.title,
+ status: t.status,
+ projectName: t.projectName,
+ projectColor: t.projectColor,
+ projectPath: t.projectPath,
+ priority: t.priority,
+ cluster: projectIndex
+ };
+ });
+
+ // Build links (dependencies)
const nodeIds = new Set(nodes.map(n => n.id));
const links = [];
- tasksWithDeps.forEach(t => {
+ filteredTasks.forEach(t => {
if (t.dependencies) {
t.dependencies.forEach(depId => {
if (nodeIds.has(depId)) {
@@ -1418,7 +1433,8 @@
// Set up SVG
const width = container.clientWidth || 800;
- const height = 500;
+ const height = Math.max(600, numProjects * 150);
+ container.style.minHeight = height + 'px';
const svg = d3.select(container)
.append('svg')
@@ -1436,7 +1452,7 @@
svg.append('defs').append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10')
- .attr('refX', 25)
+ .attr('refX', 22)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 6)
@@ -1445,13 +1461,39 @@
.attr('d', 'M 0,-5 L 10,0 L 0,5')
.attr('fill', '#64748b');
+ // Calculate cluster centers in a grid layout
+ const cols = Math.ceil(Math.sqrt(numProjects));
+ const rows = Math.ceil(numProjects / cols);
+ const clusterCenters = projectList.map((p, i) => {
+ const col = i % cols;
+ const row = Math.floor(i / cols);
+ return {
+ x: (col + 0.5) * (width / cols),
+ y: (row + 0.5) * (height / rows) + 30
+ };
+ });
+
+ // Custom cluster force to group nodes by project
+ function clusterForce(alpha) {
+ for (const node of nodes) {
+ const center = clusterCenters[node.cluster];
+ if (center) {
+ node.vx -= (node.x - center.x) * alpha * 0.1;
+ node.vy -= (node.y - center.y) * alpha * 0.1;
+ }
+ }
+ }
+
// Create force simulation
if (graphSimulation) graphSimulation.stop();
graphSimulation = d3.forceSimulation(nodes)
- .force('link', d3.forceLink(links).id(d => d.id).distance(150))
- .force('charge', d3.forceManyBody().strength(-400))
- .force('center', d3.forceCenter(width / 2, height / 2))
- .force('collision', d3.forceCollide().radius(60));
+ .force('link', d3.forceLink(links).id(d => d.id).distance(80).strength(0.5))
+ .force('charge', d3.forceManyBody().strength(-200))
+ .force('collision', d3.forceCollide().radius(45))
+ .force('cluster', clusterForce);
+
+ // Draw project group backgrounds and labels
+ const projectLabels = g.append('g').attr('class', 'project-labels');
// Draw links
const link = g.append('g')
@@ -1473,17 +1515,28 @@
.on('drag', dragged)
.on('end', dragended));
- // Node circles
- node.append('circle')
- .attr('r', 20)
+ // Node squares (rect instead of circle)
+ const nodeSize = 32;
+ node.append('rect')
+ .attr('x', -nodeSize / 2)
+ .attr('y', -nodeSize / 2)
+ .attr('width', nodeSize)
+ .attr('height', nodeSize)
+ .attr('rx', 4)
+ .attr('ry', 4)
.attr('fill', d => getStatusColor(d.status))
.attr('stroke', d => d.projectColor)
.attr('stroke-width', 3);
- // High priority indicator
+ // High priority indicator (outer square)
node.filter(d => d.priority === 'high')
- .append('circle')
- .attr('r', 24)
+ .append('rect')
+ .attr('x', -nodeSize / 2 - 4)
+ .attr('y', -nodeSize / 2 - 4)
+ .attr('width', nodeSize + 8)
+ .attr('height', nodeSize + 8)
+ .attr('rx', 6)
+ .attr('ry', 6)
.attr('fill', 'none')
.attr('stroke', '#ef4444')
.attr('stroke-width', 2)
@@ -1501,10 +1554,10 @@
// Title below node
node.append('text')
.attr('text-anchor', 'middle')
- .attr('dy', '35px')
+ .attr('dy', '28px')
.attr('fill', '#94a3b8')
- .attr('font-size', '11px')
- .text(d => d.title.length > 20 ? d.title.slice(0, 18) + '...' : d.title);
+ .attr('font-size', '9px')
+ .text(d => d.title.length > 15 ? d.title.slice(0, 13) + '...' : d.title);
// Click to show task details
node.on('click', (event, d) => {
@@ -1520,14 +1573,16 @@
// Hover effects
node.on('mouseenter', function(event, d) {
- d3.select(this).select('circle').transition().duration(150).attr('r', 25);
+ d3.select(this).selectAll('rect').transition().duration(150)
+ .attr('transform', 'scale(1.15)');
// Highlight connected links
link.attr('stroke', l => (l.source.id === d.id || l.target.id === d.id) ? '#3b82f6' : '#64748b')
.attr('stroke-width', l => (l.source.id === d.id || l.target.id === d.id) ? 3 : 2);
});
node.on('mouseleave', function(event, d) {
- d3.select(this).select('circle').transition().duration(150).attr('r', 20);
+ d3.select(this).selectAll('rect').transition().duration(150)
+ .attr('transform', 'scale(1)');
link.attr('stroke', '#64748b').attr('stroke-width', 2);
});
@@ -1539,8 +1594,67 @@
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node.attr('transform', d => `translate(${d.x},${d.y})`);
+
+ // Update project labels to center of their clusters
+ updateProjectLabels();
});
+ function updateProjectLabels() {
+ // Calculate centroid of each project's nodes
+ const centroids = projectList.map((project, i) => {
+ const projectNodes = nodes.filter(n => n.cluster === i);
+ if (projectNodes.length === 0) return null;
+ const cx = d3.mean(projectNodes, d => d.x);
+ const cy = d3.mean(projectNodes, d => d.y);
+ const minY = d3.min(projectNodes, d => d.y);
+ return { name: project.name, color: project.color, x: cx, y: minY - 35, count: projectNodes.length };
+ }).filter(c => c !== null);
+
+ // Update or create labels
+ const labels = projectLabels.selectAll('g.project-label')
+ .data(centroids, d => d.name);
+
+ const labelsEnter = labels.enter()
+ .append('g')
+ .attr('class', 'project-label');
+
+ // Background rect for label
+ labelsEnter.append('rect')
+ .attr('rx', 4)
+ .attr('ry', 4)
+ .attr('fill', d => d.color)
+ .attr('opacity', 0.9);
+
+ // Label text
+ labelsEnter.append('text')
+ .attr('text-anchor', 'middle')
+ .attr('fill', 'white')
+ .attr('font-size', '12px')
+ .attr('font-weight', 'bold')
+ .attr('dy', '0.35em');
+
+ // Merge and update
+ const labelsMerged = labelsEnter.merge(labels);
+
+ labelsMerged.attr('transform', d => `translate(${d.x},${d.y})`);
+
+ labelsMerged.select('text')
+ .text(d => `${d.name} (${d.count})`);
+
+ // Size background to fit text
+ labelsMerged.each(function(d) {
+ const text = d3.select(this).select('text');
+ const bbox = text.node().getBBox();
+ d3.select(this).select('rect')
+ .attr('x', bbox.x - 8)
+ .attr('y', bbox.y - 4)
+ .attr('width', bbox.width + 16)
+ .attr('height', bbox.height + 8);
+ });
+
+ labels.exit().remove();
+ }
+
// Drag functions
function dragstarted(event, d) {
if (!event.active) graphSimulation.alphaTarget(0.3).restart();
@@ -1558,16 +1672,16 @@
// Keep pinned after drag
}
- // Initial zoom to fit
+ // Initial zoom to fit after simulation settles
setTimeout(() => {
const bounds = g.node().getBBox();
if (bounds.width > 0) {
- const scale = Math.min(0.9, Math.min(width / (bounds.width + 100), height / (bounds.height + 100)));
+ const scale = Math.min(0.85, Math.min(width / (bounds.width + 100), height / (bounds.height + 100)));
const tx = width / 2 - (bounds.x + bounds.width / 2) * scale;
const ty = height / 2 - (bounds.y + bounds.height / 2) * scale;
svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity.translate(tx, ty).scale(scale));
}
- }, 500);
+ }, 1000);
}
// Initial connection