mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
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:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user