index.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import { apiInterceptors, getGraphVis } from '@/client/api';
  2. import { RollbackOutlined } from '@ant-design/icons';
  3. import type { Graph, GraphData, GraphOptions, ID, IPointerEvent, PluginOptions } from '@antv/g6';
  4. import { idOf } from '@antv/g6';
  5. import { Graphin } from '@antv/graphin';
  6. import { Button, Spin } from 'antd';
  7. import { groupBy } from 'lodash';
  8. import { useRouter } from 'next/router';
  9. import { useEffect, useMemo, useRef, useState } from 'react';
  10. import type { GraphVisResult } from '../../../types/knowledge';
  11. import { getDegree, getSize, isInCommunity } from '../../../utils/graph';
  12. type GraphVisData = GraphVisResult | null;
  13. const PALETTE = ['#5F95FF', '#61DDAA', '#F6BD16', '#7262FD', '#78D3F8', '#9661BC', '#F6903D', '#008685', '#F08BB4'];
  14. function GraphVis() {
  15. const LIMIT = 500;
  16. const router = useRouter();
  17. const [data, setData] = useState<GraphVisData>(null);
  18. const graphRef = useRef<Graph | null>();
  19. const [isReady, setIsReady] = useState(false);
  20. const fetchGraphVis = async () => {
  21. const [_, data] = await apiInterceptors(getGraphVis(spaceName as string, { limit: LIMIT }));
  22. setData(data);
  23. };
  24. const transformData = (data: GraphVisData): GraphData => {
  25. if (!data) return { nodes: [], edges: [] };
  26. const nodes = data.nodes.map(node => ({ id: node.id, data: node }));
  27. const edges = data.edges.map(edge => ({
  28. source: edge.source,
  29. target: edge.target,
  30. data: edge,
  31. }));
  32. return { nodes, edges };
  33. };
  34. const back = () => {
  35. router.push(`/construct/knowledge`);
  36. };
  37. const {
  38. query: { spaceName },
  39. } = useRouter();
  40. useEffect(() => {
  41. if (spaceName) fetchGraphVis();
  42. }, [spaceName]);
  43. const graphData = useMemo(() => transformData(data), [data]);
  44. useEffect(() => {
  45. if (isReady && graphRef.current) {
  46. const groupedNodes = groupBy(graphData.nodes, node => node.data!.communityId);
  47. const plugins: PluginOptions = [];
  48. Object.entries(groupedNodes).forEach(([key, nodes]) => {
  49. if (!key || nodes.length < 2) return;
  50. const color = graphRef.current?.getElementRenderStyle(idOf(nodes[0])).fill;
  51. plugins.push({
  52. key,
  53. type: 'bubble-sets',
  54. members: nodes.map(idOf),
  55. stroke: color,
  56. fill: color,
  57. fillOpacity: 0.1,
  58. });
  59. });
  60. graphRef.current.setPlugins(prev => [...prev, ...plugins]);
  61. }
  62. }, [isReady]);
  63. const getNodeSize = (nodeId: ID) => {
  64. return getSize(getNodeDegree(nodeId));
  65. };
  66. const getNodeDegree = (nodeId?: ID) => {
  67. if (!nodeId) return 0;
  68. return getDegree(graphData.edges!, nodeId);
  69. };
  70. const options: GraphOptions = {
  71. data: graphData,
  72. autoFit: 'center',
  73. node: {
  74. style: d => {
  75. const style = {
  76. size: getNodeSize(idOf(d)),
  77. label: true,
  78. labelLineWidth: 2,
  79. labelText: d.data?.name as string,
  80. labelFontSize: 10,
  81. labelBackground: true,
  82. labelBackgroundFill: '#e5e7eb',
  83. labelPadding: [0, 6],
  84. labelBackgroundRadius: 4,
  85. labelMaxWidth: '400%',
  86. labelWordWrap: true,
  87. };
  88. if (!isInCommunity(graphData, idOf(d))) {
  89. Object.assign(style, { fill: '#b0b0b0' });
  90. }
  91. return style;
  92. },
  93. state: {
  94. active: {
  95. lineWidth: 2,
  96. labelWordWrap: false,
  97. labelFontSize: 12,
  98. labelFontWeight: 'bold',
  99. },
  100. inactive: {
  101. label: false,
  102. },
  103. },
  104. palette: {
  105. type: 'group',
  106. field: 'communityId',
  107. color: PALETTE,
  108. },
  109. },
  110. edge: {
  111. style: {
  112. lineWidth: 1,
  113. stroke: '#e2e2e2',
  114. endArrow: true,
  115. endArrowType: 'vee',
  116. label: true,
  117. labelFontSize: 8,
  118. labelBackground: true,
  119. labelText: e => e.data!.name as string,
  120. labelBackgroundFill: '#e5e7eb',
  121. labelPadding: [0, 6],
  122. labelBackgroundRadius: 4,
  123. labelMaxWidth: '60%',
  124. labelWordWrap: true,
  125. },
  126. state: {
  127. active: {
  128. stroke: '#b0b0b0',
  129. labelWordWrap: false,
  130. labelFontSize: 10,
  131. labelFontWeight: 'bold',
  132. },
  133. inactive: {
  134. label: false,
  135. },
  136. },
  137. },
  138. behaviors: [
  139. 'drag-canvas',
  140. 'zoom-canvas',
  141. 'drag-element',
  142. {
  143. type: 'hover-activate',
  144. degree: 1,
  145. state: 'active',
  146. enable: (event: IPointerEvent) => ['node'].includes(event.targetType),
  147. },
  148. ],
  149. animation: false,
  150. layout: {
  151. type: 'force',
  152. preventOverlap: true,
  153. nodeSize: d => getNodeSize(d?.id as ID),
  154. linkDistance: edge => {
  155. const { source, target } = edge as { source: ID; target: ID };
  156. const nodeSize = Math.min(getNodeSize(source), getNodeSize(target));
  157. const degree = Math.min(getNodeDegree(source), getNodeDegree(target));
  158. return degree === 1 ? nodeSize * 2 : Math.min(degree * nodeSize * 1.5, 700);
  159. },
  160. },
  161. transforms: ['process-parallel-edges'],
  162. };
  163. if (!data) return <Spin className='h-full justify-center content-center' />;
  164. return (
  165. <div className='p-4 h-full overflow-y-scroll relative px-2'>
  166. <Graphin
  167. ref={ref => {
  168. graphRef.current = ref;
  169. }}
  170. style={{ height: '100%', width: '100%' }}
  171. options={options}
  172. onReady={() => {
  173. setIsReady(true);
  174. }}
  175. >
  176. <Button style={{ background: '#fff' }} onClick={back} icon={<RollbackOutlined />}>
  177. Back
  178. </Button>
  179. </Graphin>
  180. </div>
  181. );
  182. }
  183. export default GraphVis;