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(() => { useEffect(() => {
if (!containerRef.current || !data) return; if (!containerRef.current || !data) return;
// Cleanup previous instance const container = containerRef.current;
killFA2();
if (sigmaRef.current) {
sigmaRef.current.kill();
sigmaRef.current = null;
}
const graph = buildFullGraph(data); // Defer initialization to next frame so the browser computes layout first.
graphRef.current = graph; // Without this, container.offsetHeight is 0 and Sigma throws.
const rafId = requestAnimationFrame(() => {
if (!container.isConnected) return;
// Compute initial hidden set from current filters // Cleanup previous instance
const initialFilters = useReferralNetworkStore.getState().filters; killFA2();
hiddenNodesRef.current = computeHiddenNodes(graph, initialFilters); if (sigmaRef.current) {
sigmaRef.current.kill();
sigmaRef.current = null;
}
const sigma = new Sigma(graph, containerRef.current, { const graph = buildFullGraph(data);
renderEdgeLabels: false, graphRef.current = graph;
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 };
// Filter visibility (read from pre-computed set) // Compute initial hidden set from current filters
if (hiddenNodesRef.current.has(node)) { const initialFilters = useReferralNetworkStore.getState().filters;
res.hidden = true; hiddenNodesRef.current = computeHiddenNodes(graph, initialFilters);
return res;
}
const store = useReferralNetworkStore.getState(); const sigma = new Sigma(graph, container, {
const hovered = store.hoveredNodeId; allowInvalidContainer: true,
const highlighted = store.highlightedNodes; 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 // Filter visibility (read from pre-computed set)
if (highlighted.size > 0) { if (hiddenNodesRef.current.has(node)) {
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)) {
res.hidden = true; res.hidden = true;
} else { return res;
res.color = '#ffffff30';
res.size = 1;
} }
} 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; 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 return res;
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 sigmaRef.current = sigma;
fa2TimerRef.current = setTimeout(() => {
if (fa2Ref.current === supervisor) { // Expose to module-level globals for search/controls
supervisor.kill(); setSigmaInstance(sigma);
fa2Ref.current = null; 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 // Click on stage deselects
sigma.on('clickNode', ({ node }) => { sigma.on('clickStage', () => {
const attrs = graph.getNodeAttributes(node); setSelectedNode(null);
const nodeType = attrs.nodeType; });
const nodeId = attrs.nodeId;
if (typeof nodeId !== 'number') return; // Hover handler
sigma.on('enterNode', ({ node }) => {
setHoveredNode(node);
if (containerRef.current) {
containerRef.current.style.cursor = 'pointer';
}
});
if (nodeType === 'user') { sigma.on('leaveNode', () => {
setSelectedNode({ type: 'user', id: nodeId }); setHoveredNode(null);
} else if (nodeType === 'campaign') { if (containerRef.current) {
setSelectedNode({ type: 'campaign', id: nodeId }); containerRef.current.style.cursor = 'default';
} }
}); });
}); // end requestAnimationFrame
// 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';
}
});
return () => { return () => {
cancelAnimationFrame(rafId);
killFA2(); killFA2();
sigma.kill(); if (sigmaRef.current) {
sigmaRef.current = null; sigmaRef.current.kill();
sigmaRef.current = null;
}
graphRef.current = null; graphRef.current = null;
setSigmaInstance(null); setSigmaInstance(null);
setGraphInstance(null); setGraphInstance(null);