Browse Source

refactor: updateGraph

master
limina1 8 months ago
parent
commit
595424632e
  1. 377
      src/lib/navigator/EventNetwork/index.svelte

377
src/lib/navigator/EventNetwork/index.svelte

@ -228,29 +228,11 @@ @@ -228,29 +228,11 @@
.attr("stroke-width", 1);
}
/**
* Updates the graph with new data
* Generates the graph from events, creates the simulation, and renders nodes and links
* Validates that required elements are available for graph rendering
*/
function updateGraph() {
debug("updateGraph called", {
eventCount: events?.length,
starVisualization,
showTagAnchors,
selectedTagType,
disabledTagsCount: disabledTags.size
});
errorMessage = null;
// Create variables to hold our selections
let link: any;
let node: any;
let dragHandler: any;
let nodes: NetworkNode[] = [];
let links: NetworkLink[] = [];
try {
// Validate required elements
function validateGraphElements() {
if (!svg) {
throw new Error("SVG element not found");
}
@ -262,8 +244,12 @@ @@ -262,8 +244,12 @@
if (!svgGroup) {
throw new Error("SVG group not found");
}
}
// Generate graph data from events
/**
* Generates graph data from events, including tag and person anchors
*/
function generateGraphData() {
debug("Generating graph with events", {
eventCount: events.length,
currentLevels,
@ -369,23 +355,15 @@ @@ -369,23 +355,15 @@
}
}
// Save current node positions before updating
if (simulation && nodes.length > 0) {
nodes.forEach(node => {
if (node.x != null && node.y != null) {
nodePositions.set(node.id, {
x: node.x,
y: node.y,
vx: node.vx,
vy: node.vy
});
}
});
debug("Saved positions for", nodePositions.size, "nodes");
return graphData;
}
nodes = graphData.nodes;
links = graphData.links;
/**
* Filters nodes and links based on disabled tags and persons
*/
function filterNodesAndLinks(graphData: { nodes: NetworkNode[]; links: NetworkLink[] }) {
let nodes = graphData.nodes;
let links = graphData.links;
// Filter out disabled tag anchors and person nodes from nodes and links
if ((showTagAnchors && disabledTags.size > 0) || (showPersonNodes && disabledPersons.size > 0)) {
@ -432,10 +410,32 @@ @@ -432,10 +410,32 @@
});
}
// Event counts are now derived, no need to set them here
debug("Event counts by kind:", eventCounts);
return { nodes, links };
}
// Restore positions for existing nodes
/**
* Saves current node positions to preserve them across updates
*/
function saveNodePositions(nodes: NetworkNode[]) {
if (simulation && nodes.length > 0) {
nodes.forEach(node => {
if (node.x != null && node.y != null) {
nodePositions.set(node.id, {
x: node.x,
y: node.y,
vx: node.vx,
vy: node.vy
});
}
});
debug("Saved positions for", nodePositions.size, "nodes");
}
}
/**
* Restores node positions from cache and initializes new nodes
*/
function restoreNodePositions(nodes: NetworkNode[]): number {
let restoredCount = 0;
nodes.forEach(node => {
const savedPos = nodePositions.get(node.id);
@ -453,17 +453,13 @@ @@ -453,17 +453,13 @@
node.vy = 0;
}
});
debug("Generated graph data", {
nodeCount: nodes.length,
linkCount: links.length,
restoredPositions: restoredCount
});
if (!nodes.length) {
throw new Error("No nodes to render");
return restoredCount;
}
/**
* Sets up the D3 force simulation and drag handlers
*/
function setupSimulation(nodes: NetworkNode[], links: NetworkLink[], restoredCount: number) {
// Stop any existing simulation
if (simulation) {
debug("Stopping existing simulation");
@ -473,23 +469,24 @@ @@ -473,23 +469,24 @@
// Create new simulation
debug("Creating new simulation");
const hasRestoredPositions = restoredCount > 0;
let newSimulation: Simulation<NetworkNode, NetworkLink>;
if (starVisualization) {
// Use star-specific simulation
simulation = createStarSimulation(nodes, links, width, height);
newSimulation = createStarSimulation(nodes, links, width, height);
// Apply initial star positioning only if we don't have restored positions
if (!hasRestoredPositions) {
applyInitialStarPositions(nodes, links, width, height);
}
} else {
// Use regular simulation
simulation = createSimulation(nodes, links, NODE_RADIUS, LINK_DISTANCE);
newSimulation = createSimulation(nodes, links, NODE_RADIUS, LINK_DISTANCE);
// Add center force for disconnected nodes (like kind 0)
simulation.force("center", d3.forceCenter(width / 2, height / 2).strength(0.05));
newSimulation.force("center", d3.forceCenter(width / 2, height / 2).strength(0.05));
// Add radial force to keep disconnected nodes in view
simulation.force("radial", d3.forceRadial(Math.min(width, height) / 3, width / 2, height / 2)
newSimulation.force("radial", d3.forceRadial(Math.min(width, height) / 3, width / 2, height / 2)
.strength((d: NetworkNode) => {
// Apply radial force only to nodes without links (disconnected nodes)
const hasLinks = links.some(l =>
@ -502,28 +499,30 @@ @@ -502,28 +499,30 @@
// Use gentler alpha for updates with restored positions
if (hasRestoredPositions) {
simulation.alpha(0.3); // Gentler restart
newSimulation.alpha(0.3); // Gentler restart
}
// Center the nodes when the simulation is done
if (simulation) {
simulation.on("end", () => {
newSimulation.on("end", () => {
if (!starVisualization) {
centerGraph();
}
});
}
// Create drag handler
if (simulation) {
dragHandler = starVisualization
? createStarDragHandler(simulation)
: setupDragHandlers(simulation);
const dragHandler = starVisualization
? createStarDragHandler(newSimulation)
: setupDragHandlers(newSimulation);
return { simulation: newSimulation, dragHandler };
}
// Update links
/**
* Renders links in the SVG
*/
function renderLinks(links: NetworkLink[]) {
debug("Updating links");
link = svgGroup
return svgGroup
.selectAll("path.link")
.data(links, (d: NetworkLink) => `${d.source.id}-${d.target.id}`)
.join(
@ -552,79 +551,70 @@ @@ -552,79 +551,70 @@
}),
(exit: any) => exit.remove(),
);
}
// Update nodes
debug("Updating nodes");
node = svgGroup
.selectAll("g.node")
.data(nodes, (d: NetworkNode) => d.id)
.join(
(enter: any) => {
/**
* Creates the node group and attaches drag handlers
*/
function createNodeGroup(enter: any, dragHandler: any) {
const nodeEnter = enter
.append("g")
.attr("class", "node network-node-leather")
.append('g')
.attr('class', 'node network-node-leather')
.call(dragHandler);
// Larger transparent circle for better drag handling
nodeEnter
.append("circle")
.attr("class", "drag-circle")
.attr("r", NODE_RADIUS * 2.5)
.attr("fill", "transparent")
.attr("stroke", "transparent")
.style("cursor", "move");
.append('circle')
.attr('class', 'drag-circle')
.attr('r', NODE_RADIUS * 2.5)
.attr('fill', 'transparent')
.attr('stroke', 'transparent')
.style('cursor', 'move');
// Add shape based on node type
nodeEnter.each(function(d: NetworkNode) {
nodeEnter.each(function (this: SVGGElement, d: NetworkNode) {
const g = d3.select(this);
if (d.isPersonAnchor) {
// Diamond shape for person anchors
g.append("rect")
.attr("class", "visual-shape visual-diamond")
.attr("width", NODE_RADIUS * 1.5)
.attr("height", NODE_RADIUS * 1.5)
.attr("x", -NODE_RADIUS * 0.75)
.attr("y", -NODE_RADIUS * 0.75)
.attr("transform", "rotate(45)")
.attr("stroke-width", 2);
g.append('rect')
.attr('class', 'visual-shape visual-diamond')
.attr('width', NODE_RADIUS * 1.5)
.attr('height', NODE_RADIUS * 1.5)
.attr('x', -NODE_RADIUS * 0.75)
.attr('y', -NODE_RADIUS * 0.75)
.attr('transform', 'rotate(45)')
.attr('stroke-width', 2);
} else {
// Circle for other nodes
g.append("circle")
.attr("class", "visual-shape visual-circle")
.attr("r", NODE_RADIUS)
.attr("stroke-width", 2);
g.append('circle')
.attr('class', 'visual-shape visual-circle')
.attr('r', NODE_RADIUS)
.attr('stroke-width', 2);
}
});
// Node label
nodeEnter
.append("text")
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.attr("fill", "black")
.attr("font-size", "12px")
.attr("stroke", "none")
.attr("font-weight", "bold")
.style("pointer-events", "none");
.append('text')
.attr('dy', '0.35em')
.attr('text-anchor', 'middle')
.attr('fill', 'black')
.attr('font-size', '12px')
.attr('stroke', 'none')
.attr('font-weight', 'bold')
.style('pointer-events', 'none');
return nodeEnter;
},
(update: any) => {
// Ensure drag handler is applied to updated nodes
update.call(dragHandler);
return update;
},
(exit: any) => exit.remove(),
);
// Update node appearances
debug("Updating node appearances");
}
// Update visual properties for ALL nodes (both new and existing)
/**
* Updates visual properties for all nodes
*/
function updateNodeAppearance(node: any) {
node
.select(".visual-shape")
.attr("class", (d: NetworkNode) => {
const shapeClass = d.isPersonAnchor ? "visual-diamond" : "visual-circle";
.select('.visual-shape')
.attr('class', (d: NetworkNode) => {
const shapeClass = d.isPersonAnchor ? 'visual-diamond' : 'visual-circle';
const baseClasses = `visual-shape ${shapeClass} network-node-leather`;
if (d.isPersonAnchor) {
return `${baseClasses} person-anchor-node`;
@ -640,107 +630,121 @@ @@ -640,107 +630,121 @@
}
return baseClasses;
})
.style("fill", (d: NetworkNode) => {
// Person anchors - color based on source
.style('fill', (d: NetworkNode) => {
if (d.isPersonAnchor) {
// If from follow list, use kind 3 color
if (d.isFromFollowList) {
return getEventKindColor(3);
}
// Otherwise green for event authors
return "#10B981";
return '#10B981';
}
// Tag anchors get their specific colors
if (d.isTagAnchor) {
return getTagAnchorColor(d.tagType || "");
return getTagAnchorColor(d.tagType || '');
}
// Use deterministic color based on event kind
const color = getEventKindColor(d.kind);
return color;
})
.attr("opacity", 1)
.attr("r", (d: NetworkNode) => {
// Only set radius for circles
.attr('opacity', 1)
.attr('r', (d: NetworkNode) => {
if (d.isPersonAnchor) return null;
// Tag anchors are smaller
if (d.isTagAnchor) {
return NODE_RADIUS * 0.75;
}
// Make star center nodes larger
if (starVisualization && d.isContainer && d.kind === 30040) {
return NODE_RADIUS * 1.5;
}
return NODE_RADIUS;
})
.attr("width", (d: NetworkNode) => {
// Only set width/height for diamonds
.attr('width', (d: NetworkNode) => {
if (!d.isPersonAnchor) return null;
return NODE_RADIUS * 1.5;
})
.attr("height", (d: NetworkNode) => {
// Only set width/height for diamonds
.attr('height', (d: NetworkNode) => {
if (!d.isPersonAnchor) return null;
return NODE_RADIUS * 1.5;
})
.attr("x", (d: NetworkNode) => {
// Only set x/y for diamonds
.attr('x', (d: NetworkNode) => {
if (!d.isPersonAnchor) return null;
return -NODE_RADIUS * 0.75;
})
.attr("y", (d: NetworkNode) => {
// Only set x/y for diamonds
.attr('y', (d: NetworkNode) => {
if (!d.isPersonAnchor) return null;
return -NODE_RADIUS * 0.75;
})
.attr("stroke-width", (d: NetworkNode) => {
// Person anchors have thicker stroke
.attr('stroke-width', (d: NetworkNode) => {
if (d.isPersonAnchor) {
return 3;
}
// Tag anchors have thicker stroke
if (d.isTagAnchor) {
return 3;
}
return 2;
});
}
/**
* Updates the text label for all nodes
*/
function updateNodeLabels(node: any) {
node
.select("text")
.select('text')
.text((d: NetworkNode) => {
// Tag anchors show abbreviated type
if (d.isTagAnchor) {
return d.tagType === "t" ? "#" : "T";
return d.tagType === 't' ? '#' : 'T';
}
// No text for regular nodes - just show the colored circle
return "";
return '';
})
.attr("font-size", (d: NetworkNode) => {
.attr('font-size', (d: NetworkNode) => {
if (d.isTagAnchor) {
return "10px";
return '10px';
}
if (starVisualization && d.isContainer && d.kind === 30040) {
return "14px";
return '14px';
}
return "12px";
return '12px';
})
.attr("fill", (d: NetworkNode) => {
// White text on tag anchors
.attr('fill', (d: NetworkNode) => {
if (d.isTagAnchor) {
return "white";
return 'white';
}
return "black";
return 'black';
})
.style("fill", (d: NetworkNode) => {
// Force fill style for tag anchors
.style('fill', (d: NetworkNode) => {
if (d.isTagAnchor) {
return "white";
return 'white';
}
return null;
})
.attr("stroke", "none")
.style("stroke", "none");
.attr('stroke', 'none')
.style('stroke', 'none');
}
/**
* Renders nodes in the SVG (refactored for clarity)
*/
function renderNodes(nodes: NetworkNode[], dragHandler: any) {
debug('Updating nodes');
const node = svgGroup
.selectAll('g.node')
.data(nodes, (d: NetworkNode) => d.id)
.join(
(enter: any) => createNodeGroup(enter, dragHandler),
(update: any) => {
update.call(dragHandler);
return update;
},
(exit: any) => exit.remove(),
);
// Set up node interactions
updateNodeAppearance(node);
updateNodeLabels(node);
return node;
}
/**
* Sets up mouse interactions for nodes (hover and click)
*/
function setupNodeInteractions(node: any) {
debug("Setting up node interactions");
node
.on("mouseover", (event: any, d: NetworkNode) => {
@ -778,8 +782,18 @@ @@ -778,8 +782,18 @@
tooltipY = event.pageY;
}
});
}
// Set up simulation tick handler
/**
* Sets up the simulation tick handler for animation
*/
function setupSimulationTickHandler(
simulation: Simulation<NetworkNode, NetworkLink> | null,
nodes: NetworkNode[],
links: NetworkLink[],
link: any,
node: any
) {
debug("Setting up simulation tick handler");
if (simulation) {
simulation.on("tick", () => {
@ -837,10 +851,55 @@ @@ -837,10 +851,55 @@
);
});
}
} catch (error) {
}
/**
* Handles errors that occur during graph updates
*/
function handleGraphError(error: unknown) {
console.error("Error in updateGraph:", error);
errorMessage = `Error updating graph: ${error instanceof Error ? error.message : String(error)}`;
}
/**
* Updates the graph with new data
* Generates the graph from events, creates the simulation, and renders nodes and links
*/
function updateGraph() {
debug("updateGraph called", {
eventCount: events?.length,
starVisualization,
showTagAnchors,
selectedTagType,
disabledTagsCount: disabledTags.size
});
errorMessage = null;
try {
validateGraphElements();
const graphData = generateGraphData();
// Save current positions before filtering
saveNodePositions(graphData.nodes);
const { nodes, links } = filterNodesAndLinks(graphData);
const restoredCount = restoreNodePositions(nodes);
if (!nodes.length) {
throw new Error("No nodes to render");
}
const { simulation: newSimulation, dragHandler } = setupSimulation(nodes, links, restoredCount);
simulation = newSimulation;
const link = renderLinks(links);
const node = renderNodes(nodes, dragHandler);
setupNodeInteractions(node);
setupSimulationTickHandler(simulation, nodes, links, link, node);
} catch (error) {
handleGraphError(error);
}
}
/**

Loading…
Cancel
Save