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