68 lines
2.7 KiB
TypeScript
68 lines
2.7 KiB
TypeScript
import React, { useState, useRef, useMemo, useEffect, type FC } from 'react'
|
|
import Empty from '@/components/Empty'
|
|
import { GRAPH_COLORS, initCommunityGraph } from './utils'
|
|
import { useD3Graph } from './hooks'
|
|
import type { CommunityD3Node, D3Link, CommunityGraphProps } from './types'
|
|
|
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
// Renders a D3-powered community graph with optional tooltip and legend.
|
|
|
|
const CommunityGraph: FC<CommunityGraphProps> = ({
|
|
data,
|
|
empty: emptyProp,
|
|
colors = GRAPH_COLORS,
|
|
renderTooltip,
|
|
showLegend = true,
|
|
onCommunityClick,
|
|
onNodeClick,
|
|
defaultZoom = 1,
|
|
}) => {
|
|
// Tooltip position and hovered node state
|
|
const [tooltip, setTooltip] = useState<{ x: number; y: number; node: CommunityD3Node } | null>(null)
|
|
|
|
// Keep callback refs stable to avoid re-initializing the graph on every render
|
|
const onCommunityClickRef = useRef(onCommunityClick)
|
|
const onNodeClickRef = useRef(onNodeClick)
|
|
const renderTooltipRef = useRef(renderTooltip)
|
|
useEffect(() => { onCommunityClickRef.current = onCommunityClick }, [onCommunityClick])
|
|
useEffect(() => { onNodeClickRef.current = onNodeClick }, [onNodeClick])
|
|
useEffect(() => { renderTooltipRef.current = renderTooltip }, [renderTooltip])
|
|
|
|
const graphState = useMemo(() => data, [data])
|
|
// Show empty state when explicitly flagged or when there are no nodes
|
|
const isEmpty = emptyProp ?? !data?.nodes.length
|
|
|
|
// Initialize (or re-initialize) the D3 graph whenever relevant state changes
|
|
const containerRef = useD3Graph((container) => {
|
|
if (!graphState) return
|
|
return initCommunityGraph(
|
|
container,
|
|
graphState.nodes,
|
|
graphState.links as D3Link[],
|
|
graphState.communityMap,
|
|
graphState.communityCaption,
|
|
graphState.communityNodeMap,
|
|
{ colors, showLegend, defaultZoom, setTooltip: renderTooltip ? setTooltip : () => {}, onCommunityClickRef, onNodeClickRef }
|
|
)
|
|
}, [graphState, showLegend, defaultZoom])
|
|
|
|
// Resolve tooltip content: use custom renderer if provided, otherwise fall back to DefaultTooltip
|
|
const tooltipNode = tooltip && renderTooltipRef.current
|
|
? renderTooltipRef.current(tooltip.node)
|
|
: null
|
|
|
|
if (isEmpty) return <Empty className="rb:h-full" />
|
|
return (
|
|
<div className="rb:w-full rb:h-full rb:relative">
|
|
<div ref={containerRef} className="rb:w-full rb:h-full" />
|
|
{tooltipNode ? (
|
|
<div style={{ position: 'absolute', left: tooltip!.x + 14, top: tooltip!.y - 10, pointerEvents: 'none', zIndex: 20 }}>
|
|
{tooltipNode}
|
|
</div>
|
|
) : undefined}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default React.memo(CommunityGraph)
|