From 100ff87fbc46b0f5ce2f6c7ff1ebb5f7e9ef2586 Mon Sep 17 00:00:00 2001 From: LZS911 <932177767@qq.com> Date: Wed, 19 Feb 2025 16:22:17 +0800 Subject: [PATCH] [chore](sqle/knowledge) optimization graph --- jest.config.js | 9 +- .../page/DataSource/components/List/index.tsx | 2 +- packages/base/vite.config.mts | 2 +- .../lib/testUtil/mockSigmaNodeImage.tsx | 1 - packages/sqle/package.json | 4 +- .../sqle/src/page/GlobalDashboard/index.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 76 +++++++++++ .../Knowledge/Graph/__tests__/index.test.tsx | 23 ++++ .../src/page/Knowledge/Graph/common/data.ts | 53 ------- .../page/Knowledge/Graph/components/Fa2.tsx | 36 +++++ .../{common => components}/FocusOnNode.tsx | 0 .../{common => components}/LoadGraph.tsx | 46 +++++-- .../{common => components}/NodePopover.tsx | 2 +- .../Graph/hooks/__tests__/useGraph.test.ts | 129 ++++++++++++++++++ .../page/Knowledge/Graph/hooks/useGraph.ts | 56 ++++---- .../sqle/src/page/Knowledge/Graph/index.tsx | 67 +++++++-- .../src/page/Knowledge/Graph/index.type.ts | 3 +- pnpm-lock.yaml | 109 +++++++++++---- 18 files changed, 484 insertions(+), 136 deletions(-) delete mode 100644 packages/shared/lib/testUtil/mockSigmaNodeImage.tsx create mode 100644 packages/sqle/src/page/Knowledge/Graph/__tests__/__snapshots__/index.test.tsx.snap create mode 100644 packages/sqle/src/page/Knowledge/Graph/__tests__/index.test.tsx delete mode 100644 packages/sqle/src/page/Knowledge/Graph/common/data.ts create mode 100644 packages/sqle/src/page/Knowledge/Graph/components/Fa2.tsx rename packages/sqle/src/page/Knowledge/Graph/{common => components}/FocusOnNode.tsx (100%) rename packages/sqle/src/page/Knowledge/Graph/{common => components}/LoadGraph.tsx (71%) rename packages/sqle/src/page/Knowledge/Graph/{common => components}/NodePopover.tsx (99%) create mode 100644 packages/sqle/src/page/Knowledge/Graph/hooks/__tests__/useGraph.test.ts diff --git a/jest.config.js b/jest.config.js index 5cf3c846c..c262a1c5e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,8 +15,7 @@ module.exports = { }, transformIgnorePatterns: [ '/dist/', - // Ignore modules without es dir. - 'node_modules/(?!(?:.pnpm/)?(.+/es))[^/]+?/(?!(es|node_modules)/)' + 'node_modules/(?!(?:.pnpm/)?(@react-sigma|.+/es))[^/]+?/(?!(es|node_modules)/)' ], moduleFileExtensions: ['ts', 'tsx', 'js', 'json', 'jsx', 'node'], testEnvironment: 'jest-environment-jsdom', @@ -35,10 +34,9 @@ module.exports = { '/packages/shared/lib/testUtil/mockSigmaCore.tsx', '@react-sigma/graph-search$': '/packages/shared/lib/testUtil/mockSigmaGraphSearch.tsx', - '@sigma/node-image$': - '/packages/shared/lib/testUtil/mockSigmaNodeImage.tsx', ...pathsToModuleNameMapper(compilerOptions.paths) }, + collectCoverageFrom: [ 'packages/**/{src,lib}/{page,components,hooks,global,store,utils}/**/*.{ts,tsx}', 'packages/**/src/App.tsx', @@ -46,7 +44,8 @@ module.exports = { '!packages/**/index.type.ts', '!packages/**/index.enum.ts', '!packages/sqle/src/page/SqlAnalyze/SqlAnalyze/ProcessListCom/**', - '!packages/shared/lib/hooks/usePrompt/index.tsx' + '!packages/shared/lib/hooks/usePrompt/index.tsx', + '!packages/sqle/src/page/Knowledge/Graph/components/**' ], setupFilesAfterEnv: ['/jest-setup.ts'], reporters: [ diff --git a/packages/base/src/page/DataSource/components/List/index.tsx b/packages/base/src/page/DataSource/components/List/index.tsx index 304201572..222af51f9 100644 --- a/packages/base/src/page/DataSource/components/List/index.tsx +++ b/packages/base/src/page/DataSource/components/List/index.tsx @@ -220,7 +220,7 @@ const DataSourceList = () => { hide(); }); }, - [messageApi, modalApi, projectID, t] + [messageApi, modalApi, projectID, refresh, t] ); const batchTestDatabaseConnection = () => { diff --git a/packages/base/vite.config.mts b/packages/base/vite.config.mts index 833412baa..1d64a64ea 100644 --- a/packages/base/vite.config.mts +++ b/packages/base/vite.config.mts @@ -114,7 +114,7 @@ export default defineConfig((config) => { open: true, proxy: { '^(/v|/sqle/v)': { - target: 'http://10.186.62.32:9999' + target: 'http://10.186.62.13:11000/' }, '^/provision/v': { target: 'http://10.186.62.13:11000/' diff --git a/packages/shared/lib/testUtil/mockSigmaNodeImage.tsx b/packages/shared/lib/testUtil/mockSigmaNodeImage.tsx deleted file mode 100644 index 572c7248f..000000000 --- a/packages/shared/lib/testUtil/mockSigmaNodeImage.tsx +++ /dev/null @@ -1 +0,0 @@ -export const NodeImageProgram = () => null; diff --git a/packages/sqle/package.json b/packages/sqle/package.json index e29879b3e..76415cd47 100644 --- a/packages/sqle/package.json +++ b/packages/sqle/package.json @@ -9,9 +9,11 @@ "@react-sigma/core": "^5.0.2", "@react-sigma/graph-search": "^5.0.3", "@react-sigma/layout-force": "^5.0.2", - "@sigma/node-image": "^3.0.0", + "@react-sigma/layout-forceatlas2": "^5.0.2", "@uiw/react-md-editor": "^3.23.5", "graphology": "^0.25.4", + "graphology-communities-louvain": "^2.0.2", + "iwanthue": "^2.0.0", "react-grid-layout": "^1.3.4", "react-infinite-scroll-component": "^6.1.0", "rehype-sanitize": "^6.0.0", diff --git a/packages/sqle/src/page/GlobalDashboard/index.tsx b/packages/sqle/src/page/GlobalDashboard/index.tsx index e6d9f92f0..e50323029 100644 --- a/packages/sqle/src/page/GlobalDashboard/index.tsx +++ b/packages/sqle/src/page/GlobalDashboard/index.tsx @@ -5,7 +5,7 @@ import { } from '@actiontech/shared'; import useDashboardFilter from './hooks/useDashboardFilter'; import GlobalDashboardTableFilter from './components/TableFilter'; -import { useState, useMemo, useEffect } from 'react'; +import { useState, useMemo } from 'react'; import { GlobalDashBoardSegmentedEnum } from './index.type'; import { useTranslation } from 'react-i18next'; import { Space } from 'antd'; diff --git a/packages/sqle/src/page/Knowledge/Graph/__tests__/__snapshots__/index.test.tsx.snap b/packages/sqle/src/page/Knowledge/Graph/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000..5fd85d7ad --- /dev/null +++ b/packages/sqle/src/page/Knowledge/Graph/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KnowledgeGraph should match snapshot 1`] = ` + + + + + + + + + + + + + + + + + +`; diff --git a/packages/sqle/src/page/Knowledge/Graph/__tests__/index.test.tsx b/packages/sqle/src/page/Knowledge/Graph/__tests__/index.test.tsx new file mode 100644 index 000000000..704cf7186 --- /dev/null +++ b/packages/sqle/src/page/Knowledge/Graph/__tests__/index.test.tsx @@ -0,0 +1,23 @@ +import { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import KnowledgeGraph from '..'; + +describe('KnowledgeGraph', () => { + const mockGraphData = { + nodes: [], + edges: [] + }; + + beforeEach(() => {}); + + afterEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + }); + + it('should match snapshot', () => { + expect( + toJson(shallow()) + ).toMatchSnapshot(); + }); +}); diff --git a/packages/sqle/src/page/Knowledge/Graph/common/data.ts b/packages/sqle/src/page/Knowledge/Graph/common/data.ts deleted file mode 100644 index 96f73c767..000000000 --- a/packages/sqle/src/page/Knowledge/Graph/common/data.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { SigmaContainerProps } from '@react-sigma/core'; -import { EdgeType, NodeType } from '../index.type'; -import { Attributes } from 'graphology-types'; -import { NodeProgramType } from 'sigma/rendering'; -import { NodeImageProgram } from '@sigma/node-image'; - -export const sigmaSettings: SigmaContainerProps< - NodeType, - EdgeType, - Attributes ->['settings'] = { - // 允许在无效容器中渲染 (默认值: false) - allowInvalidContainer: true, - defaultNodeType: 'image', - // 设置边的默认类型 (默认值: 'line') - defaultEdgeType: 'line', - - // 相机缩放比例限制 (默认值: min: 0.1, max: 32) - minCameraRatio: 0.2, - maxCameraRatio: 5, - - // 节点渲染程序类 (默认值: {}) - nodeProgramClasses: { - image: NodeImageProgram as unknown as NodeProgramType< - NodeType, - EdgeType, - Attributes - > - }, - - // 渲染性能优化 - hideEdgesOnMove: true, // 移动时隐藏边 (默认值: false) - hideLabelsOnMove: false, // 移动时隐藏标签 (默认值: false) - renderLabels: true, // 是否渲染标签 (默认值: true) - renderEdgeLabels: false, // 是否渲染边标签 (默认值: false) - - labelFont: `'PlusJakartaSans Medium', -apple-system, 'Microsoft YaHei', - BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', - sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji`, - labelSize: 14, - // 标签渲染优化 - labelDensity: 0.07, // 标签密度 (默认值: 0.03) - labelGridCellSize: 100, // 标签网格大小 (默认值: 100) - labelRenderedSizeThreshold: 8, // 标签大小阈值 (默认值: 6) - - // 默认样式 - defaultNodeColor: '#1a73e8', // 节点颜色 (默认值: '#999') - defaultEdgeColor: '#ccc', // 边颜色 (默认值: '#ccc') - - // 交互体验优化 - zoomToSizeRatioFunction: (x: number) => x // 缩放比例函数 (默认值: Math.pow) -}; diff --git a/packages/sqle/src/page/Knowledge/Graph/components/Fa2.tsx b/packages/sqle/src/page/Knowledge/Graph/components/Fa2.tsx new file mode 100644 index 000000000..4e3987865 --- /dev/null +++ b/packages/sqle/src/page/Knowledge/Graph/components/Fa2.tsx @@ -0,0 +1,36 @@ +import { useEffect } from 'react'; +import { useWorkerLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'; + +const Fa2: React.FC = () => { + const { start, kill, stop } = useWorkerLayoutForceAtlas2({ + settings: { + slowDown: 10, + gravity: 1, // 增加重力,防止节点飞散 + strongGravityMode: false, + scalingRatio: 2, // 增加节点间斥力 + linLogMode: true, // 使用对数模式,更好地处理大图 + outboundAttractionDistribution: true, // 基于度数分布吸引力 + adjustSizes: true // 考虑节点大小 + } + }); + + useEffect(() => { + // start FA2 + start(); + + const timer = setTimeout(() => { + stop(); + }, 10000); + + // Kill FA2 on unmount + return () => { + if (timer) { + clearTimeout(timer); + } + kill(); + }; + }, [start, kill, stop]); + + return null; +}; +export default Fa2; diff --git a/packages/sqle/src/page/Knowledge/Graph/common/FocusOnNode.tsx b/packages/sqle/src/page/Knowledge/Graph/components/FocusOnNode.tsx similarity index 100% rename from packages/sqle/src/page/Knowledge/Graph/common/FocusOnNode.tsx rename to packages/sqle/src/page/Knowledge/Graph/components/FocusOnNode.tsx diff --git a/packages/sqle/src/page/Knowledge/Graph/common/LoadGraph.tsx b/packages/sqle/src/page/Knowledge/Graph/components/LoadGraph.tsx similarity index 71% rename from packages/sqle/src/page/Knowledge/Graph/common/LoadGraph.tsx rename to packages/sqle/src/page/Knowledge/Graph/components/LoadGraph.tsx index 4c26d994f..88112e566 100644 --- a/packages/sqle/src/page/Knowledge/Graph/common/LoadGraph.tsx +++ b/packages/sqle/src/page/Knowledge/Graph/components/LoadGraph.tsx @@ -4,7 +4,7 @@ import { useSetSettings, useSigma } from '@react-sigma/core'; -import { FC, useEffect } from 'react'; +import { FC, useEffect, useState } from 'react'; import useGraph from '../hooks/useGraph'; import { EdgeType, NodeType } from '../index.type'; import { @@ -34,6 +34,7 @@ const LoadGraph: FC = ({ const setSettings = useSetSettings(); const loadGraph = useLoadGraph(); const { sharedTheme } = useThemeStyleData(); + const [clickedNode, setClickedNode] = useState(null); useEffect(() => { if (graphData) { @@ -41,23 +42,24 @@ const LoadGraph: FC = ({ loadGraph(graph); onLoaded?.(); - const handleEnterNode = (event: any) => { - setHoveredNode(event.node); - }; - - const handleLeaveNode = () => { - setHoveredNode(null); - }; - registerEvents({ - enterNode: handleEnterNode, - leaveNode: handleLeaveNode + enterNode: (event) => { + setHoveredNode(event.node); + }, + leaveNode: () => { + setHoveredNode(null); + }, + clickNode: (event) => { + setClickedNode((nodeId) => + nodeId === event.node ? null : event.node + ); + } }); return () => { - // 清理事件监听 sigma.removeAllListeners('enterNode'); sigma.removeAllListeners('leaveNode'); + sigma.removeAllListeners('clickNode'); }; } }, [ @@ -79,6 +81,18 @@ const LoadGraph: FC = ({ highlighted: data.highlighted || false }; + if (clickedNode && graph.hasNode(clickedNode)) { + const isNeighbor = + node === clickedNode || graph.neighbors(clickedNode).includes(node); + if (isNeighbor) { + newData.highlighted = true; + } else { + newData.color = sharedTheme.uiToken.colorFillSecondary; + newData.highlighted = false; + } + return newData; + } + if (hoveredNode && graph.hasNode(hoveredNode)) { const isNeighbor = node === hoveredNode || graph.neighbors(hoveredNode).includes(node); @@ -106,7 +120,13 @@ const LoadGraph: FC = ({ return newData; } }); - }, [hoveredNode, setSettings, sharedTheme.uiToken.colorFillSecondary, sigma]); + }, [ + clickedNode, + hoveredNode, + setSettings, + sharedTheme.uiToken.colorFillSecondary, + sigma + ]); return null; }; diff --git a/packages/sqle/src/page/Knowledge/Graph/common/NodePopover.tsx b/packages/sqle/src/page/Knowledge/Graph/components/NodePopover.tsx similarity index 99% rename from packages/sqle/src/page/Knowledge/Graph/common/NodePopover.tsx rename to packages/sqle/src/page/Knowledge/Graph/components/NodePopover.tsx index a7cb63192..bdce319c3 100644 --- a/packages/sqle/src/page/Knowledge/Graph/common/NodePopover.tsx +++ b/packages/sqle/src/page/Knowledge/Graph/components/NodePopover.tsx @@ -50,7 +50,7 @@ const NodePopover: React.FC = ({ nodeId }) => { // 考虑左侧菜单栏和顶部区域的偏移 const MENU_WIDTH = 270; - const HEADER_HEIGHT = 160; + const HEADER_HEIGHT = 156; const finalPosition = { x: containerRect.left + viewportPos.x - MENU_WIDTH, diff --git a/packages/sqle/src/page/Knowledge/Graph/hooks/__tests__/useGraph.test.ts b/packages/sqle/src/page/Knowledge/Graph/hooks/__tests__/useGraph.test.ts new file mode 100644 index 000000000..c219867b0 --- /dev/null +++ b/packages/sqle/src/page/Knowledge/Graph/hooks/__tests__/useGraph.test.ts @@ -0,0 +1,129 @@ +import Graph, { MultiDirectedGraph } from 'graphology'; +import louvain from 'graphology-communities-louvain'; +import iwanthue from 'iwanthue'; +import useGraph from '../useGraph'; +import { renderHook } from '@testing-library/react'; + +jest.mock('graphology-communities-louvain', () => ({ + assign: jest.fn() +})); + +jest.mock('iwanthue', () => jest.fn()); + +describe('useGraph', () => { + const mockTheme = { + sqleTheme: { + knowledgeTheme: { + graph: { + edge: { + color: '#666666' + } + } + } + } + }; + + beforeEach(() => { + (iwanthue as jest.Mock).mockReturnValue(['#FF0000', '#00FF00']); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('calculateNormalizedSize', () => { + it('should calculate normalized size when max equals min', () => { + const { result } = renderHook(() => useGraph()); + const mockData = { + nodes: [{ id: '1', weight: 5 }], + edges: [] + }; + const graph = result.current.createGraph(mockData); + expect(graph.getNodeAttribute('1', 'size')).toBe(19); // (10 + 28) / 2 + }); + + it('should calculate normalized size when max not equals min', () => { + const { result } = renderHook(() => useGraph()); + const mockData = { + nodes: [ + { id: '1', weight: 1 }, + { id: '2', weight: 5 } + ], + edges: [] + }; + const graph = result.current.createGraph(mockData); + expect(graph.getNodeAttribute('1', 'size')).toBe(10); // min size + expect(graph.getNodeAttribute('2', 'size')).toBe(28); // max size + }); + }); + + describe('getWeightRange', () => { + it('should handle empty array', () => { + const { result } = renderHook(() => useGraph()); + const mockData = { + nodes: [], + edges: [] + }; + const graph = result.current.createGraph(mockData); + expect(graph).toBeInstanceOf(Graph); + }); + + it('should handle undefined weights', () => { + const { result } = renderHook(() => useGraph()); + const mockData = { + nodes: [{ id: '1' }, { id: '2' }], + edges: [] + }; + const graph = result.current.createGraph(mockData); + expect(graph.getNodeAttribute('1', 'size')).toBe(19); // (10 + 28) / 2 + }); + }); + + describe('createGraph', () => { + it('should create graph with nodes and edges', () => { + const { result } = renderHook(() => useGraph()); + const mockData = { + nodes: [ + { id: '1', name: 'Node1', weight: 3 }, + { id: '2', name: 'Node2', weight: 5 } + ], + edges: [{ from_id: '1', to_id: '2', weight: 2 }] + }; + + const graph = result.current.createGraph(mockData); + + // 验证节点属性 + expect(graph.hasNode('1')).toBeTruthy(); + expect(graph.hasNode('2')).toBeTruthy(); + expect(graph.getNodeAttribute('1', 'label')).toBe('Node1'); + expect(graph.getNodeAttribute('1', 'shortLabel')).toBe('N'); + + // 验证边属性 + expect(graph.hasEdge('1', '2')).toBeTruthy(); + }); + + it('should handle community detection and color assignment', () => { + const { result } = renderHook(() => useGraph()); + const mockData = { + nodes: [ + { id: '1', name: 'Node1' }, + { id: '2', name: 'Node2' } + ], + edges: [{ from_id: '1', to_id: '2' }] + }; + + // @ts-ignore + louvain.assign.mockImplementation((graph) => { + graph.forEachNode((node: string) => { + graph.setNodeAttribute(node, 'community', '0'); + }); + }); + + const graph = result.current.createGraph(mockData); + + expect(louvain.assign).toHaveBeenCalled(); + expect(iwanthue).toHaveBeenCalled(); + expect(graph.getNodeAttribute('1', 'color')).toBe('#FF0000'); + }); + }); +}); diff --git a/packages/sqle/src/page/Knowledge/Graph/hooks/useGraph.ts b/packages/sqle/src/page/Knowledge/Graph/hooks/useGraph.ts index 459821972..036a697ae 100644 --- a/packages/sqle/src/page/Knowledge/Graph/hooks/useGraph.ts +++ b/packages/sqle/src/page/Knowledge/Graph/hooks/useGraph.ts @@ -5,19 +5,10 @@ import { INodeResponse } from '@actiontech/shared/lib/api/sqle/service/common'; import { EdgeType, NodeType } from '../index.type'; -import useThemeStyleData from '../../../../hooks/useThemeStyleData'; +import louvain from 'graphology-communities-louvain'; +import iwanthue from 'iwanthue'; const useGraph = () => { - const { sqleTheme } = useThemeStyleData(); - const generateNodeColor = useCallback(() => { - const digits = '0123456789abcdef'; - let code = '#'; - for (let i = 0; i < 6; i++) { - code += digits.charAt(Math.floor(Math.random() * 16)); - } - return code; - }, []); - const calculateNormalizedSize = useCallback( ( value: number, @@ -48,12 +39,13 @@ const useGraph = () => { const createGraph = useCallback( (data: { nodes: INodeResponse[]; edges: IEdgeResponse[] }) => { const graph = new MultiDirectedGraph(); + const communities = new Set(); const nodeCount = data.nodes.length; - const MIN_NODE_SIZE = 5; - const MAX_NODE_SIZE = 20; + const MIN_NODE_SIZE = 10; + const MAX_NODE_SIZE = 28; const MIN_EDGE_SIZE = 2; - const MAX_EDGE_SIZE = 8; + const MAX_EDGE_SIZE = 16; const nodeWeightRange = getWeightRange(data.nodes); const edgeWeightRange = getWeightRange(data.edges); @@ -74,14 +66,13 @@ const useGraph = () => { graph.addNode(node.id, { label: node.name ?? '', size: normalizedSize, - color: generateNodeColor(), x: 0.5 + radius * Math.cos(theta), y: 0.5 + radius * Math.sin(theta), - shortLabel: (node.name ?? '')[0]?.toUpperCase() ?? '', - image: '/static/image/knowledge.svg' + shortLabel: (node.name ?? '')[0]?.toUpperCase() ?? '' }); }); + // 添加边 data.edges.forEach((edge) => { const normalizedSize = calculateNormalizedSize( edge.weight ?? 1, @@ -93,19 +84,34 @@ const useGraph = () => { graph.addEdge(edge.from_id, edge.to_id, { size: normalizedSize, - forceLabel: false, - color: sqleTheme.knowledgeTheme.graph.edge.color + forceLabel: false }); }); + /** + * - 参考至:https://www.sigmajs.org/storybook/?path=/story/sigma-utils--fit-viewport-to-nodes + * - louvain.assign() 是一个社区检测算法,它会自动将相互联系紧密的节点分为一组,给每个节点添加 community 属性 + * - iwanthue 是一个颜色生成库,用于生成互相区分度高的颜色 + */ + louvain.assign(graph, { nodeCommunityAttribute: 'community' }); + graph.forEachNode((_, attrs) => { + communities.add(attrs.community!); + }); + const communitiesArray = Array.from(communities); + const palette: Record = iwanthue(communities.size).reduce( + (iter, color, i) => ({ + ...iter, + [communitiesArray[i]]: color + }), + {} + ); + graph.forEachNode((node, attr) => + graph.setNodeAttribute(node, 'color', palette[attr.community!]) + ); + return graph as Graph; }, - [ - getWeightRange, - calculateNormalizedSize, - generateNodeColor, - sqleTheme.knowledgeTheme.graph.edge.color - ] + [getWeightRange, calculateNormalizedSize] ); return { createGraph }; diff --git a/packages/sqle/src/page/Knowledge/Graph/index.tsx b/packages/sqle/src/page/Knowledge/Graph/index.tsx index e47885914..b7e99f72a 100644 --- a/packages/sqle/src/page/Knowledge/Graph/index.tsx +++ b/packages/sqle/src/page/Knowledge/Graph/index.tsx @@ -2,22 +2,62 @@ import { ControlsContainer, FullScreenControl, SigmaContainer, + SigmaContainerProps, ZoomControl } from '@react-sigma/core'; import '@react-sigma/core/lib/style.css'; import { GraphSearch, GraphSearchOption } from '@react-sigma/graph-search'; import '@react-sigma/graph-search/lib/style.css'; -import FocusOnNode from './common/FocusOnNode'; -import LoadGraph from './common/LoadGraph'; +import FocusOnNode from './components/FocusOnNode'; +import LoadGraph from './components/LoadGraph'; import '@react-sigma/core/lib/style.css'; -import { sigmaSettings } from './common/data'; -import { useCallback, useEffect, useState } from 'react'; +import { Attributes, useCallback, useEffect, useState } from 'react'; import { KnowledgeGraphStyleWrapper } from './style'; -import { KnowledgeGraphProp } from './index.type'; +import { EdgeType, KnowledgeGraphProp, NodeType } from './index.type'; import classNames from 'classnames'; -import NodePopover from './common/NodePopover'; +import NodePopover from './components/NodePopover'; import { Spin } from 'antd'; import { useTranslation } from 'react-i18next'; +import Fa2 from './components/Fa2'; + +const sigmaSettings: SigmaContainerProps< + NodeType, + EdgeType, + Attributes +>['settings'] = { + // 允许在无效容器中渲染 (默认值: false) + allowInvalidContainer: true, + defaultNodeType: 'circle', + // 设置边的默认类型 (默认值: 'line') + defaultEdgeType: 'line', + + // 节点渲染程序类 (默认值: {}) + nodeProgramClasses: {}, + + // 渲染性能优化 + hideEdgesOnMove: false, // 移动时隐藏边 (默认值: false) + hideLabelsOnMove: false, // 移动时隐藏标签 (默认值: false) + renderLabels: true, // 是否渲染标签 (默认值: true) + renderEdgeLabels: false, // 是否渲染边标签 (默认值: false) + + labelFont: `'PlusJakartaSans Medium', -apple-system, 'Microsoft YaHei', + BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', + sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji`, + labelSize: 14, + + // 交互体验优化 + zoomToSizeRatioFunction: (x: number) => x, // 缩放比例函数 (默认值: Math.pow) + + // 调整缩放比例限制 + minCameraRatio: 0.1, + maxCameraRatio: 10, + + // 调整标签渲染 + labelDensity: 0.1, // 增加标签密度 + labelGridCellSize: 150, // 增加标签网格大小 + labelRenderedSizeThreshold: 8 // 标签大小阈值 (默认值: 6) +}; const KnowledgeGraph: React.FC = ({ graphData }) => { const { t } = useTranslation(); @@ -28,13 +68,19 @@ const KnowledgeGraph: React.FC = ({ graphData }) => { const [isLoading, setIsLoading] = useState(true); const onFocus = useCallback((value: GraphSearchOption | null) => { - if (value === null) setFocusNode(null); - else if (value.type === 'nodes') setFocusNode(value.id); + if (value === null) { + setFocusNode(null); + } else if (value.type === 'nodes') { + setFocusNode(value.id); + } }, []); const onChange = useCallback((value: GraphSearchOption | null) => { - if (value === null) setSelectedNode(null); - else if (value.type === 'nodes') setSelectedNode(value.id); + if (value === null) { + setSelectedNode(null); + } else if (value.type === 'nodes') { + setSelectedNode(value.id); + } }, []); const handleGraphLoaded = useCallback(() => { @@ -80,6 +126,7 @@ const KnowledgeGraph: React.FC = ({ graphData }) => { })} settings={sigmaSettings} > + =16.9.0' dependencies: '@babel/runtime': 7.23.6 - classnames: 2.3.2 + classnames: 2.5.1 rc-util: 5.42.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -4432,7 +4438,7 @@ packages: dependencies: '@babel/runtime': 7.23.6 '@rc-component/portal': 1.1.1(react-dom@18.2.0)(react@18.2.0) - classnames: 2.3.2 + classnames: 2.5.1 rc-motion: 2.7.3(react-dom@18.2.0)(react@18.2.0) rc-resize-observer: 1.3.1(react-dom@18.2.0)(react@18.2.0) rc-util: 5.42.0(react-dom@18.2.0)(react@18.2.0) @@ -4448,7 +4454,7 @@ packages: dependencies: '@babel/runtime': 7.23.6 '@rc-component/portal': 1.1.1(react-dom@18.2.0)(react@18.2.0) - classnames: 2.3.2 + classnames: 2.5.1 rc-motion: 2.7.3(react-dom@18.2.0)(react@18.2.0) rc-resize-observer: 1.3.1(react-dom@18.2.0)(react@18.2.0) rc-util: 5.42.0(react-dom@18.2.0)(react@18.2.0) @@ -4506,6 +4512,19 @@ packages: - sigma dev: false + /@react-sigma/layout-forceatlas2@5.0.2(graphology-layout-forceatlas2@0.10.1)(graphology@0.25.4)(react@18.2.0)(sigma@3.0.1): + resolution: {integrity: sha512-+x05LMmjVOtwiu9EJ7CbgtRVDo/0jNT6dXqjLWvXPVy7BD/uRF4iMs/1KhSmnR5R9TTbznkpUd4iX7o/CLkxRg==} + peerDependencies: + graphology-layout-forceatlas2: ^0.10.1 + dependencies: + '@react-sigma/layout-core': 5.0.2(graphology@0.25.4)(react@18.2.0)(sigma@3.0.1) + graphology-layout-forceatlas2: 0.10.1(graphology-types@0.24.8) + transitivePeerDependencies: + - graphology + - react + - sigma + dev: false + /@reduxjs/toolkit@1.9.3(react-redux@8.0.5)(react@18.2.0): resolution: {integrity: sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg==} peerDependencies: @@ -4696,14 +4715,6 @@ packages: selderee: 0.11.0 dev: true - /@sigma/node-image@3.0.0(sigma@3.0.1): - resolution: {integrity: sha512-i4WLNPugDY4jgQEZtNSiSVj4HHXOraciXLtlgdygeUxMVEhH8PJ/+Q1vQ9f/SlKFnZQ+7vH3HnsSDW6FD9aP+g==} - peerDependencies: - sigma: '>=3.0.0-beta.10' - dependencies: - sigma: 3.0.1(graphology-types@0.24.8) - dev: false - /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true @@ -8156,6 +8167,9 @@ packages: /classnames@2.3.2: resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} + /classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + /clean-css@5.3.3: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} @@ -11489,6 +11503,28 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true + /graphology-communities-louvain@2.0.2(graphology-types@0.24.8): + resolution: {integrity: sha512-zt+2hHVPYxjEquyecxWXoUoIuN/UvYzsvI7boDdMNz0rRvpESQ7+e+Ejv6wK7AThycbZXuQ6DkG8NPMCq6XwoA==} + peerDependencies: + graphology-types: '>=0.19.0' + dependencies: + graphology-indices: 0.17.0(graphology-types@0.24.8) + graphology-types: 0.24.8 + graphology-utils: 2.5.2(graphology-types@0.24.8) + mnemonist: 0.39.8 + pandemonium: 2.4.1 + dev: false + + /graphology-indices@0.17.0(graphology-types@0.24.8): + resolution: {integrity: sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ==} + peerDependencies: + graphology-types: '>=0.20.0' + dependencies: + graphology-types: 0.24.8 + graphology-utils: 2.5.2(graphology-types@0.24.8) + mnemonist: 0.39.8 + dev: false + /graphology-layout-force@0.2.4(graphology-types@0.24.8): resolution: {integrity: sha512-NYZz0YAnDkn5pkm30cvB0IScFoWGtbzJMrqaiH070dYlYJiag12Oc89dbVfaMaVR/w8DMIKxn/ix9Bqj+Umm9Q==} peerDependencies: @@ -11498,6 +11534,15 @@ packages: graphology-utils: 2.5.2(graphology-types@0.24.8) dev: false + /graphology-layout-forceatlas2@0.10.1(graphology-types@0.24.8): + resolution: {integrity: sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ==} + peerDependencies: + graphology-types: '>=0.19.0' + dependencies: + graphology-types: 0.24.8 + graphology-utils: 2.5.2(graphology-types@0.24.8) + dev: false + /graphology-types@0.24.8: resolution: {integrity: sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==} @@ -12783,6 +12828,12 @@ packages: set-function-name: 2.0.2 dev: true + /iwanthue@2.0.0: + resolution: {integrity: sha512-baARnKbEygsic78ekXFxvEfVyiWPchkeoUYy6StEhjF4ayCanBZQhEiXlzPXIUXtgVCYyvwex0dnw1Ghg8UCCg==} + dependencies: + obliterator: 2.0.5 + dev: false + /jackspeak@3.4.0: resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==} engines: {node: '>=14'} @@ -14713,6 +14764,12 @@ packages: minimist: 1.2.8 dev: true + /mnemonist@0.39.8: + resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==} + dependencies: + obliterator: 2.0.5 + dev: false + /mockdate@3.0.5: resolution: {integrity: sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==} dev: true @@ -15424,6 +15481,12 @@ packages: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} dev: true + /pandemonium@2.4.1: + resolution: {integrity: sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==} + dependencies: + mnemonist: 0.39.8 + dev: false + /parallel-transform@1.2.0: resolution: {integrity: sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==} dependencies: @@ -16620,7 +16683,7 @@ packages: react-dom: '>=16.9.0' dependencies: '@babel/runtime': 7.23.6 - classnames: 2.3.2 + classnames: 2.5.1 dom-align: 1.12.4 rc-util: 5.42.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 @@ -16813,7 +16876,7 @@ packages: dependencies: '@babel/runtime': 7.23.6 '@rc-component/trigger': 1.18.3(react-dom@18.2.0)(react@18.2.0) - classnames: 2.3.2 + classnames: 2.5.1 rc-motion: 2.7.3(react-dom@18.2.0)(react@18.2.0) rc-overflow: 1.3.1(react-dom@18.2.0)(react@18.2.0) rc-util: 5.42.0(react-dom@18.2.0)(react@18.2.0) @@ -16828,7 +16891,7 @@ packages: react-dom: '>=16.9.0' dependencies: '@babel/runtime': 7.23.6 - classnames: 2.3.2 + classnames: 2.5.1 rc-util: 5.42.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -16855,7 +16918,7 @@ packages: react-dom: '>=16.9.0' dependencies: '@babel/runtime': 7.23.6 - classnames: 2.3.2 + classnames: 2.5.1 rc-resize-observer: 1.3.1(react-dom@18.2.0)(react@18.2.0) rc-util: 5.42.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 @@ -16937,7 +17000,7 @@ packages: react-dom: '>=16.9.0' dependencies: '@babel/runtime': 7.23.6 - classnames: 2.3.2 + classnames: 2.5.1 rc-util: 5.42.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -17040,7 +17103,7 @@ packages: react-dom: '>=16.9.0' dependencies: '@babel/runtime': 7.23.6 - classnames: 2.3.2 + classnames: 2.5.1 rc-dropdown: 4.1.0(react-dom@18.2.0)(react@18.2.0) rc-menu: 9.12.4(react-dom@18.2.0)(react@18.2.0) rc-motion: 2.7.3(react-dom@18.2.0)(react@18.2.0) @@ -17104,7 +17167,7 @@ packages: dependencies: '@babel/runtime': 7.23.6 '@rc-component/trigger': 2.2.0(react-dom@18.2.0)(react@18.2.0) - classnames: 2.3.2 + classnames: 2.5.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true @@ -17132,7 +17195,7 @@ packages: react-dom: '*' dependencies: '@babel/runtime': 7.23.6 - classnames: 2.3.2 + classnames: 2.5.1 rc-motion: 2.7.3(react-dom@18.2.0)(react@18.2.0) rc-util: 5.42.0(react-dom@18.2.0)(react@18.2.0) rc-virtual-list: 3.5.3(react-dom@18.2.0)(react@18.2.0) @@ -17195,7 +17258,7 @@ packages: react-dom: '*' dependencies: '@babel/runtime': 7.23.6 - classnames: 2.3.2 + classnames: 2.5.1 rc-resize-observer: 1.3.1(react-dom@18.2.0)(react@18.2.0) rc-util: 5.42.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0