fix: defer Sigma init with requestAnimationFrame to prevent no-height crash

Container has no computed height when useEffect fires immediately
after data arrives. Defer to next animation frame so browser layout
completes. Also add allowInvalidContainer as safety net.
This commit is contained in:
Fringg
2026-03-19 08:56:31 +03:00
parent 3434073d7c
commit 6f58a0ce5d

View File

@@ -128,172 +128,184 @@ export function NetworkGraph({ data, className }: NetworkGraphProps) {
useEffect(() => {
if (!containerRef.current || !data) return;
// Cleanup previous instance
killFA2();
if (sigmaRef.current) {
sigmaRef.current.kill();
sigmaRef.current = null;
}
const container = containerRef.current;
const graph = buildFullGraph(data);
graphRef.current = graph;
// Defer initialization to next frame so the browser computes layout first.
// Without this, container.offsetHeight is 0 and Sigma throws.
const rafId = requestAnimationFrame(() => {
if (!container.isConnected) return;
// Compute initial hidden set from current filters
const initialFilters = useReferralNetworkStore.getState().filters;
hiddenNodesRef.current = computeHiddenNodes(graph, initialFilters);
// Cleanup previous instance
killFA2();
if (sigmaRef.current) {
sigmaRef.current.kill();
sigmaRef.current = null;
}
const sigma = new Sigma(graph, containerRef.current, {
renderEdgeLabels: false,
labelDensity: 0.5,
labelRenderedSizeThreshold: 6,
zIndex: true,
defaultEdgeColor: '#ffffff10',
defaultNodeColor: '#6b7280',
labelColor: { color: '#e8e6f0' },
labelFont: 'Inter, system-ui, sans-serif',
labelSize: 12,
stagePadding: 40,
nodeReducer: (node, attrs) => {
const res = { ...attrs };
const graph = buildFullGraph(data);
graphRef.current = graph;
// Filter visibility (read from pre-computed set)
if (hiddenNodesRef.current.has(node)) {
res.hidden = true;
return res;
}
// Compute initial hidden set from current filters
const initialFilters = useReferralNetworkStore.getState().filters;
hiddenNodesRef.current = computeHiddenNodes(graph, initialFilters);
const store = useReferralNetworkStore.getState();
const hovered = store.hoveredNodeId;
const highlighted = store.highlightedNodes;
const sigma = new Sigma(graph, container, {
allowInvalidContainer: true,
renderEdgeLabels: false,
labelDensity: 0.5,
labelRenderedSizeThreshold: 6,
zIndex: true,
defaultEdgeColor: '#ffffff10',
defaultNodeColor: '#6b7280',
labelColor: { color: '#e8e6f0' },
labelFont: 'Inter, system-ui, sans-serif',
labelSize: 12,
stagePadding: 40,
nodeReducer: (node, attrs) => {
const res = { ...attrs };
// Search highlighting
if (highlighted.size > 0) {
if (highlighted.has(node)) {
res.highlighted = true;
res.zIndex = 2;
} else {
res.color = `${attrs.color}33`;
res.label = '';
res.zIndex = 0;
}
}
// Hover highlighting
if (hovered) {
if (node === hovered) {
res.highlighted = true;
res.zIndex = 2;
} else if (graph.hasNode(hovered) && graph.areNeighbors(node, hovered)) {
res.highlighted = true;
res.zIndex = 1;
} else if (!highlighted.has(node)) {
res.color = `${attrs.color}33`;
res.label = '';
res.zIndex = 0;
}
}
return res;
},
edgeReducer: (edge, attrs) => {
const res = { ...attrs };
// Hide edges connected to filtered-out nodes
const [source, target] = graph.extremities(edge);
if (hiddenNodesRef.current.has(source) || hiddenNodesRef.current.has(target)) {
res.hidden = true;
return res;
}
const store = useReferralNetworkStore.getState();
const hovered = store.hoveredNodeId;
const highlighted = store.highlightedNodes;
if (hovered && graph.hasNode(hovered)) {
if (!graph.hasExtremity(edge, hovered)) {
// Filter visibility (read from pre-computed set)
if (hiddenNodesRef.current.has(node)) {
res.hidden = true;
} else {
res.color = '#ffffff30';
res.size = 1;
return res;
}
} else if (highlighted.size > 0) {
if (!highlighted.has(source) && !highlighted.has(target)) {
const store = useReferralNetworkStore.getState();
const hovered = store.hoveredNodeId;
const highlighted = store.highlightedNodes;
// Search highlighting
if (highlighted.size > 0) {
if (highlighted.has(node)) {
res.highlighted = true;
res.zIndex = 2;
} else {
res.color = `${attrs.color}33`;
res.label = '';
res.zIndex = 0;
}
}
// Hover highlighting
if (hovered) {
if (node === hovered) {
res.highlighted = true;
res.zIndex = 2;
} else if (graph.hasNode(hovered) && graph.areNeighbors(node, hovered)) {
res.highlighted = true;
res.zIndex = 1;
} else if (!highlighted.has(node)) {
res.color = `${attrs.color}33`;
res.label = '';
res.zIndex = 0;
}
}
return res;
},
edgeReducer: (edge, attrs) => {
const res = { ...attrs };
// Hide edges connected to filtered-out nodes
const [source, target] = graph.extremities(edge);
if (hiddenNodesRef.current.has(source) || hiddenNodesRef.current.has(target)) {
res.hidden = true;
return res;
}
}
return res;
},
});
const store = useReferralNetworkStore.getState();
const hovered = store.hoveredNodeId;
const highlighted = store.highlightedNodes;
sigmaRef.current = sigma;
if (hovered && graph.hasNode(hovered)) {
if (!graph.hasExtremity(edge, hovered)) {
res.hidden = true;
} else {
res.color = '#ffffff30';
res.size = 1;
}
} else if (highlighted.size > 0) {
if (!highlighted.has(source) && !highlighted.has(target)) {
res.hidden = true;
}
}
// Expose to module-level globals for search/controls
setSigmaInstance(sigma);
setGraphInstance(graph);
// Start ForceAtlas2 in a web worker (non-blocking)
if (graph.order > 0) {
const supervisor = new FA2LayoutSupervisor(graph, {
settings: {
gravity: 0.5,
scalingRatio: 10,
barnesHutOptimize: true,
slowDown: 2,
return res;
},
});
fa2Ref.current = supervisor;
supervisor.start();
// Kill after a fixed duration to free the web worker thread
fa2TimerRef.current = setTimeout(() => {
if (fa2Ref.current === supervisor) {
supervisor.kill();
fa2Ref.current = null;
sigmaRef.current = sigma;
// Expose to module-level globals for search/controls
setSigmaInstance(sigma);
setGraphInstance(graph);
// Start ForceAtlas2 in a web worker (non-blocking)
if (graph.order > 0) {
const supervisor = new FA2LayoutSupervisor(graph, {
settings: {
gravity: 0.5,
scalingRatio: 10,
barnesHutOptimize: true,
slowDown: 2,
},
});
fa2Ref.current = supervisor;
supervisor.start();
// Kill after a fixed duration to free the web worker thread
fa2TimerRef.current = setTimeout(() => {
if (fa2Ref.current === supervisor) {
supervisor.kill();
fa2Ref.current = null;
}
fa2TimerRef.current = null;
}, FA2_DURATION_MS);
}
// Click handler
sigma.on('clickNode', ({ node }) => {
const attrs = graph.getNodeAttributes(node);
const nodeType = attrs.nodeType;
const nodeId = attrs.nodeId;
if (typeof nodeId !== 'number') return;
if (nodeType === 'user') {
setSelectedNode({ type: 'user', id: nodeId });
} else if (nodeType === 'campaign') {
setSelectedNode({ type: 'campaign', id: nodeId });
}
fa2TimerRef.current = null;
}, FA2_DURATION_MS);
}
});
// Click handler
sigma.on('clickNode', ({ node }) => {
const attrs = graph.getNodeAttributes(node);
const nodeType = attrs.nodeType;
const nodeId = attrs.nodeId;
// Click on stage deselects
sigma.on('clickStage', () => {
setSelectedNode(null);
});
if (typeof nodeId !== 'number') return;
// Hover handler
sigma.on('enterNode', ({ node }) => {
setHoveredNode(node);
if (containerRef.current) {
containerRef.current.style.cursor = 'pointer';
}
});
if (nodeType === 'user') {
setSelectedNode({ type: 'user', id: nodeId });
} else if (nodeType === 'campaign') {
setSelectedNode({ type: 'campaign', id: nodeId });
}
});
// Click on stage deselects
sigma.on('clickStage', () => {
setSelectedNode(null);
});
// Hover handler
sigma.on('enterNode', ({ node }) => {
setHoveredNode(node);
if (containerRef.current) {
containerRef.current.style.cursor = 'pointer';
}
});
sigma.on('leaveNode', () => {
setHoveredNode(null);
if (containerRef.current) {
containerRef.current.style.cursor = 'default';
}
});
sigma.on('leaveNode', () => {
setHoveredNode(null);
if (containerRef.current) {
containerRef.current.style.cursor = 'default';
}
});
}); // end requestAnimationFrame
return () => {
cancelAnimationFrame(rafId);
killFA2();
sigma.kill();
sigmaRef.current = null;
if (sigmaRef.current) {
sigmaRef.current.kill();
sigmaRef.current = null;
}
graphRef.current = null;
setSigmaInstance(null);
setGraphInstance(null);