|
|
|
|
@ -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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|