Files
taskpile/frontend/node_modules/force-graph/example/build-a-graph/index.html
Alvis f1d51b8cc8 Add side panels, task selection, graph animation, and project docs
- Foldable left panel (user profile) and right panel (task details)
- Clicking a task in the list or graph node selects it and shows details
- Both views (task list + graph) always mounted via absolute inset-0 for
  correct canvas dimensions; tabs toggle visibility with opacity
- Graph node selection animation: other nodes repel outward (charge -600),
  then selected node smoothly slides to center (500ms cubic ease-out),
  then charge restores to -120 and graph stabilizes
- Graph re-fits on tab switch and panel resize via ResizeObserver
- Fix UUID string IDs throughout (backend returns UUIDs, not integers)
- Add TaskDetailPanel, UserPanel components
- Add CLAUDE.md project documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 11:23:06 +00:00

108 lines
3.8 KiB
HTML

<head>
<style> body { margin: 0; } </style>
<script src="//cdn.jsdelivr.net/npm/force-graph"></script>
<!--<script src="../../dist/force-graph.js"></script>-->
</head>
<body>
<br/>
<div style="text-align: center; color: silver">
<b>New node:</b> click on the canvas, <b>New link:</b> drag one node close enough to another one,
<b>Rename</b> node or link by clicking on it, <b>Remove</b> node or link by right-clicking on it
</div>
<div id="graph"></div>
<script>
let nodeIdCounter = 0, linkIdCounter = 0;
let nodes = [], links = [];
let dragSourceNode = null, interimLink = null;
const snapInDistance = 15;
const snapOutDistance = 40;
const updateGraphData = () => {
Graph.graphData({ nodes: nodes, links: links });
};
const distance = (node1, node2) => {
return Math.sqrt(Math.pow(node1.x - node2.x, 2) + Math.pow(node1.y - node2.y, 2));
};
const rename = (nodeOrLink, type) => {
let value = prompt('Name this ' + type + ':', nodeOrLink.name);
if (!value) {
return;
}
nodeOrLink.name = value;
updateGraphData();
};
const setInterimLink = (source, target) => {
let linkId = linkIdCounter ++;
interimLink = { id: linkId, source: source, target: target, name: 'link_' + linkId };
links.push(interimLink);
updateGraphData();
};
const removeLink = link => {
links.splice(links.indexOf(link), 1);
};
const removeInterimLinkWithoutAddingIt = () => {
removeLink(interimLink);
interimLink = null;
updateGraphData();
};
const removeNode = node => {
links.filter(link => link.source === node || link.target === node).forEach(link => removeLink(link));
nodes.splice(nodes.indexOf(node), 1);
};
const Graph = new ForceGraph(document.getElementById('graph'))
.linkDirectionalArrowLength(6)
.linkDirectionalArrowRelPos(1)
.onNodeDrag(dragNode => {
dragSourceNode = dragNode;
for (let node of nodes) {
if (dragNode === node) {
continue;
}
// close enough: snap onto node as target for suggested link
if (!interimLink && distance(dragNode, node) < snapInDistance) {
setInterimLink(dragSourceNode, node);
}
// close enough to other node: snap over to other node as target for suggested link
if (interimLink && node !== interimLink.target && distance(dragNode, node) < snapInDistance) {
removeLink(interimLink);
setInterimLink(dragSourceNode, node);
}
}
// far away enough: snap out of the current target node
if (interimLink && distance(dragNode, interimLink.target) > snapOutDistance) {
removeInterimLinkWithoutAddingIt();
}
})
.onNodeDragEnd(() => {
dragSourceNode = null;
interimLink = null;
updateGraphData();
})
.nodeColor(node => node === dragSourceNode || (interimLink &&
(node === interimLink.source || node === interimLink.target)) ? 'orange' : null)
.linkColor(link => link === interimLink ? 'orange' : '#bbbbbb')
.linkLineDash(link => link === interimLink ? [2, 2] : [])
.onNodeClick((node, event) => rename(node, 'node'))
.onNodeRightClick((node, event) => removeNode(node))
.onLinkClick((link, event) => rename(link, 'link'))
.onLinkRightClick((link, event) => removeLink(link))
.onBackgroundClick(event => {
let coords = Graph.screen2GraphCoords(event.layerX, event.layerY);
let nodeId = nodeIdCounter ++;
nodes.push({ id: nodeId, x: coords.x, y: coords.y, name: 'node_' + nodeId });
updateGraphData();
});
updateGraphData();
</script>
</body>