Update dependency graph: squares, all tasks, project clusters
- Change nodes from circles to squares (rounded corners) - Show ALL tasks, not just those with dependencies - Group tasks by project using cluster force simulation - Add project name labels hovering above each cluster - Labels show project name and task count - Dynamic height based on number of projects - Legend updated to show square shapes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
36b35e3cbe
commit
19f669f7d1
|
|
@ -594,10 +594,10 @@
|
|||
</div>
|
||||
<div id="dependency-graph"></div>
|
||||
<div class="graph-legend">
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#22c55e"></span> Done</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#3b82f6"></span> In Progress</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#6b7280"></span> To Do</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:transparent;border:2px solid #94a3b8"></span> Border = Project</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#22c55e;border-radius:2px"></span> Done</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#3b82f6;border-radius:2px"></span> In Progress</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#6b7280;border-radius:2px"></span> To Do</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:transparent;border:2px solid #94a3b8;border-radius:2px"></span> Border = Project</div>
|
||||
</div>
|
||||
<div class="graph-hint">Drag nodes to reposition • Double-click to unpin • Scroll to zoom • Click node for details</div>
|
||||
</div>
|
||||
|
|
@ -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 = '<div style="display:flex;align-items:center;justify-content:center;height:400px;color:#64748b;font-size:1rem;">No tasks with dependencies to display.</div>';
|
||||
if (filteredTasks.length === 0) {
|
||||
container.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:400px;color:#64748b;font-size:1rem;">No tasks to display.</div>';
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue