|
import { ElementDatum, Graph, IElementEvent } from '@antv/g6'; |
|
import isEmpty from 'lodash/isEmpty'; |
|
import { useCallback, useEffect, useMemo, useRef } from 'react'; |
|
import { buildNodesAndCombos } from './util'; |
|
|
|
import styles from './index.less'; |
|
|
|
const TooltipColorMap = { |
|
combo: 'red', |
|
node: 'black', |
|
edge: 'blue', |
|
}; |
|
|
|
interface IProps { |
|
data: any; |
|
show: boolean; |
|
} |
|
|
|
const ForceGraph = ({ data, show }: IProps) => { |
|
const containerRef = useRef<HTMLDivElement>(null); |
|
const graphRef = useRef<Graph | null>(null); |
|
|
|
const nextData = useMemo(() => { |
|
if (!isEmpty(data)) { |
|
const graphData = data; |
|
const mi = buildNodesAndCombos(graphData.nodes); |
|
return { edges: graphData.edges, ...mi }; |
|
} |
|
return { nodes: [], edges: [] }; |
|
}, [data]); |
|
|
|
const render = useCallback(() => { |
|
const graph = new Graph({ |
|
container: containerRef.current!, |
|
autoFit: 'view', |
|
autoResize: true, |
|
behaviors: [ |
|
'drag-element', |
|
'drag-canvas', |
|
'zoom-canvas', |
|
'collapse-expand', |
|
{ |
|
type: 'hover-activate', |
|
degree: 1, |
|
}, |
|
], |
|
plugins: [ |
|
{ |
|
type: 'tooltip', |
|
enterable: true, |
|
getContent: (e: IElementEvent, items: ElementDatum) => { |
|
if (Array.isArray(items)) { |
|
if (items.some((x) => x?.isCombo)) { |
|
return `<p style="font-weight:600;color:red">${items?.[0]?.data?.label}</p>`; |
|
} |
|
let result = ``; |
|
items.forEach((item) => { |
|
result += `<section style="color:${TooltipColorMap[e['targetType'] as keyof typeof TooltipColorMap]};"><h3>${item?.id}</h3>`; |
|
if (item?.entity_type) { |
|
result += `<div style="padding-bottom: 6px;"><b>Entity type: </b>${item?.entity_type}</div>`; |
|
} |
|
if (item?.weight) { |
|
result += `<div><b>Weight: </b>${item?.weight}</div>`; |
|
} |
|
if (item?.description) { |
|
result += `<p>${item?.description}</p>`; |
|
} |
|
}); |
|
return result + '</section>'; |
|
} |
|
return undefined; |
|
}, |
|
}, |
|
], |
|
layout: { |
|
type: 'combo-combined', |
|
preventOverlap: true, |
|
comboPadding: 1, |
|
spacing: 100, |
|
}, |
|
node: { |
|
style: { |
|
size: 150, |
|
labelText: (d) => d.id, |
|
|
|
labelFontSize: 40, |
|
|
|
labelOffsetY: 20, |
|
labelPlacement: 'center', |
|
labelWordWrap: true, |
|
}, |
|
palette: { |
|
type: 'group', |
|
field: (d) => { |
|
return d?.entity_type as string; |
|
}, |
|
}, |
|
}, |
|
edge: { |
|
style: (model) => { |
|
const weight: number = Number(model?.weight) || 2; |
|
const lineWeight = weight * 4; |
|
return { |
|
stroke: '#99ADD1', |
|
lineWidth: lineWeight > 10 ? 10 : lineWeight, |
|
}; |
|
}, |
|
}, |
|
}); |
|
|
|
if (graphRef.current) { |
|
graphRef.current.destroy(); |
|
} |
|
|
|
graphRef.current = graph; |
|
|
|
graph.setData(nextData); |
|
|
|
graph.render(); |
|
}, [nextData]); |
|
|
|
useEffect(() => { |
|
if (!isEmpty(data)) { |
|
render(); |
|
} |
|
}, [data, render]); |
|
|
|
return ( |
|
<div |
|
ref={containerRef} |
|
className={styles.forceContainer} |
|
style={{ |
|
width: '100%', |
|
height: '100%', |
|
display: show ? 'block' : 'none', |
|
}} |
|
/> |
|
); |
|
}; |
|
|
|
export default ForceGraph; |
|
|