├── public ├── favicon.ico └── index.html ├── tsconfig.porduction.json ├── babel.config.js ├── example ├── main.ts └── App.vue ├── shims-vue.d.ts ├── src ├── components │ ├── index.ts │ ├── Tooltip │ │ ├── index.md │ │ ├── getContainerStyles.ts │ │ ├── index.ts │ │ └── useTooltip.ts │ ├── Legend │ │ ├── typing.ts │ │ ├── index.ts │ │ ├── useLegend.ts │ │ └── Node.ts │ ├── ContextMenu │ │ ├── index.md │ │ ├── index.ts │ │ └── useContextMenu.ts │ ├── SnapLine │ │ └── index.ts │ ├── FishEye │ │ └── index.ts │ ├── MiniMap │ │ └── index.ts │ └── Hull │ │ └── index.ts ├── behaviors │ ├── index.ts │ ├── ClickSelect.ts │ ├── FontPaint.ts │ ├── DragCanvas.ts │ ├── DragCombo.ts │ ├── DragNode.ts │ ├── LassoSelect.ts │ ├── ActivateRelations.ts │ ├── ZoomCanvas.ts │ ├── BrushSelect.ts │ ├── useBehaviorHook.ts │ ├── FitView.ts │ ├── ResizeCanvas.ts │ ├── Hoverable.ts │ ├── DragNodeWithForce.ts │ └── TreeCollapse.ts ├── GraphinContext.ts ├── registers.ts ├── index.ts └── Graphin.ts ├── .gitignore ├── .npmignore ├── tsconfig.json ├── LICENSE ├── README.md └── package.json /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lloydzhou/graphin-vue/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /tsconfig.porduction.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | plugins: ["@vue/babel-plugin-jsx"] 6 | } 7 | -------------------------------------------------------------------------------- /example/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | const app = createApp(App) 5 | app.config.unwrapInjectedRef = true 6 | app.mount('#app') 7 | 8 | -------------------------------------------------------------------------------- /shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ContextMenu'; 2 | export * from './FishEye'; 3 | export * from './Hull'; 4 | export * from './Legend'; 5 | export * from './MiniMap'; 6 | export * from './SnapLine'; 7 | export * from './Tooltip'; 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/behaviors/index.ts: -------------------------------------------------------------------------------- 1 | /** 内置 */ 2 | export * from './DragCanvas' 3 | export * from './ZoomCanvas' 4 | export * from './ClickSelect' 5 | export * from './BrushSelect' 6 | export * from './DragNode' 7 | export * from './ResizeCanvas' 8 | export * from './LassoSelect' 9 | export * from './DragCombo' 10 | export * from './Hoverable' 11 | /** 可选 */ 12 | export * from './ActivateRelations' 13 | export * from './TreeCollapse' 14 | export * from './FitView' 15 | export * from './FontPaint' 16 | export * from './DragNodeWithForce' 17 | 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | package-lock.json 3 | yarn.lock 4 | /.git/ 5 | /.vscode/ 6 | *.log 7 | 8 | .DS_Store 9 | /src 10 | /dist 11 | /example 12 | /node_modules 13 | /public 14 | /tests 15 | .browserslistrc 16 | vue.config.js 17 | 18 | # local env files 19 | .env.local 20 | .env.*.local 21 | 22 | # Log files 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Editor directories and files 28 | .idea 29 | .vscode 30 | .gitignore 31 | .npmignore 32 | .npmrc 33 | *.suo 34 | *.ntvs* 35 | *.njsproj 36 | *.sln 37 | *.sw? 38 | -------------------------------------------------------------------------------- /src/GraphinContext.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { provide, inject } from 'vue' 3 | 4 | export const contextSymbol = String(Symbol('contextSymbol')) 5 | 6 | export const createContext = (context) => { 7 | provide(contextSymbol, context) 8 | } 9 | 10 | export const useContext = () => { 11 | const context = inject(contextSymbol) 12 | if (!context) { 13 | throw new Error('context must be used after useProvide') 14 | } 15 | return context 16 | } 17 | 18 | export default { 19 | createContext, 20 | contextSymbol, 21 | useContext, 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/components/Tooltip/index.md: -------------------------------------------------------------------------------- 1 | # Tooltip 2 | 3 | Tooltip 提示框是一种快速浏览信息的交互组件,常用于图的节点和边上。通过鼠标悬停在节点或边上时,出现一个提示框,鼠标移出节点则取消提示框。这在快速查询元素详细信息时非常有帮助。 4 | 5 | ``是提示框的容器,提供位置,触发元素,回调函数等信息,这些信息可以用户通过`context`获取。具体如何渲染,完全交给用户 6 | 7 | ## demo 8 | 9 | ``` 10 | // 模板中使用slot.default可以拿到tooltip中的值,包含item, node 11 | 12 | 21 | 22 | ``` 23 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/behaviors/ClickSelect.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import useBehaviorHook, { BehaviorComponent } from './useBehaviorHook' 3 | 4 | const DEFAULT_TRIGGER = 'shift' 5 | // const ALLOW_EVENTS = ['shift', 'ctrl', 'alt', 'control']; 6 | const defaultConfig = { 7 | /** 是否禁用该功能 */ 8 | disabled: false, 9 | /** 是否允许多选,默认为 true,当设置为 false,表示不允许多选,此时 trigger 参数无效; */ 10 | multiple: true, 11 | /** 指定按住哪个键进行多选,默认为 shift,按住 Shift 键多选,用户可配置 shift、ctrl、alt; */ 12 | trigger: DEFAULT_TRIGGER, 13 | /** 选中的样式,默认为 selected */ 14 | selectedState: 'selected' 15 | } 16 | export type ClickSelectProps = typeof defaultConfig 17 | export const ClickSelect: BehaviorComponent = useBehaviorHook({ 18 | name: 'ClickSelect', 19 | type: 'click-select', 20 | defaultConfig 21 | }) 22 | -------------------------------------------------------------------------------- /src/behaviors/FontPaint.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { onMounted, onUnmounted, defineComponent, ref, DefineComponent } from 'vue' 3 | import { useContext, contextSymbol } from '../GraphinContext' 4 | 5 | export const FontPaint: DefineComponent = defineComponent({ 6 | name: 'FontPaint', 7 | inject: [contextSymbol], 8 | setup () { 9 | const { graph } = useContext() 10 | const timer = ref() 11 | onMounted(() => { 12 | timer.value = setTimeout(() => { 13 | graph.getNodes().forEach((node) => { 14 | graph.setItemState(node, 'normal', true) 15 | }) 16 | graph.paint() 17 | }, 1600) 18 | }) 19 | onUnmounted(() => { 20 | if (timer.value) { 21 | clearTimeout(timer.value) 22 | } 23 | }) 24 | return () => null 25 | } 26 | }) 27 | 28 | -------------------------------------------------------------------------------- /src/components/Legend/typing.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'vue' 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | export interface LegendProps { 4 | /** 绑定的类型 */ 5 | bindType: 'node' | 'edge'; 6 | /** 7 | * @description 分类映射的Key值 8 | */ 9 | sortKey: string; 10 | /** 11 | * @description 颜色映射的Key值 12 | * @default "style.stroke" 13 | */ 14 | colorKey?: string; 15 | /** 16 | * @description 样式 17 | */ 18 | style?: CSSProperties; 19 | } 20 | export interface OptionType { 21 | /** 颜色 */ 22 | color: string; 23 | /** 值 */ 24 | value: string | number; 25 | /** 标签 */ 26 | label: string; 27 | /** 是否选中 */ 28 | checked: boolean; 29 | } 30 | 31 | export interface LegendChildrenProps { 32 | bindType: string; 33 | sortKey: string; 34 | dataMap: Map; 35 | options: OptionType[]; 36 | } 37 | -------------------------------------------------------------------------------- /src/behaviors/DragCanvas.ts: -------------------------------------------------------------------------------- 1 | import useBehaviorHook, { BehaviorComponent } from './useBehaviorHook' 2 | 3 | const defaultConfig = { 4 | /** 允许拖拽方向,支持'x','y','both',默认方向为 'both'; */ 5 | direction: 'both', 6 | /** 是否开启优化,开启后拖动画布过程中隐藏所有的边及节点上非 keyShape 部分,默认关闭; */ 7 | enableOptimize: false, 8 | /** 9 | * drag-canvas 可拖动的扩展范围,默认为 0,即最多可以拖动一屏的位置 10 | * 当设置的值大于 0 时,即拖动可以超过一屏 11 | * 当设置的值小于 0 时,相当于缩小了可拖动范围 12 | * 具体实例可参考:https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*IFfoS67_HssAAAAAAAAAAAAAARQnAQ 13 | */ 14 | scalableRange: 0, 15 | /** 是否允许触发该操作; */ 16 | shouldBegin: () => { 17 | return true 18 | }, 19 | /** 是否禁用该功能 */ 20 | disabled: false 21 | } 22 | export type DragCanvasProps = typeof defaultConfig 23 | export const DragCanvas: BehaviorComponent = useBehaviorHook({ 24 | name: 'DragCanvas', 25 | type: 'drag-canvas', 26 | defaultConfig 27 | }) 28 | -------------------------------------------------------------------------------- /src/behaviors/DragCombo.ts: -------------------------------------------------------------------------------- 1 | import useBehaviorHook, { BehaviorComponent } from './useBehaviorHook' 2 | 3 | const defaultConfig = { 4 | /** 是否禁用该功能 */ 5 | disabled: false, 6 | /** 拖动 Combo 时候是否开启图形代理 delegate,即拖动 Combo 时候 Combo 不会实时跟随变动,拖动过程中有临时生成一个 delegate 图形,拖动结束后才更新 Combo 位置,默认为 false,不开启 */ 7 | enableDelegate: false, 8 | /** delegate 的样式 */ 9 | delegateStyle: {}, 10 | /** 拖动嵌套的 Combo 时,只改变父 Combo 的大小,不改变层级关系,默认为 false; */ 11 | onlyChangeComboSize: false, 12 | /** 当拖动 Combo 时,父 Combo 或进入到的 Combo 的状态值,需要用户在实例化 Graph 时在 comboStateStyles 里面配置,默认为空; */ 13 | activeState: '', 14 | /** 选中 Combo 的状态,默认为 selected,需要在 comboStateStyles 里面配置; */ 15 | selectedState: 'selected' 16 | } 17 | export type DragComboProps = typeof defaultConfig 18 | export const DragCombo: BehaviorComponent = useBehaviorHook({ 19 | name: 'DragCombo', 20 | type: 'drag-combo', 21 | defaultConfig 22 | }) 23 | -------------------------------------------------------------------------------- /src/components/ContextMenu/index.md: -------------------------------------------------------------------------------- 1 | # ContextMenu 右键菜单 2 | 3 | ContextMenu 是右键菜单,通常是对节点进行进一步操作的组件。例如:通过右键菜单实现节点的复制、删除、反选等。同时,用户也可以对选中的节点进行进一步打标、分析、关系扩散、数据请求等高级的交互行为。图分析产品中的右键菜单往往是和浏览器网页的右键菜单交互与展示形式保持一致,但在展示形式上也可以有更多特殊的设计,如仪表盘形状的菜单 4 | 5 | ## demo 6 | 7 | ``` 8 | import { Menu } from 'ant-design-vue'; 9 | import 'ant-design-vue/es/menu/style/css' 10 | 11 | const MenuItem = Menu.Item 12 | 13 | 14 | // 定义点击回调 15 | hendleContextMenuClick(data, ev) { 16 | console.log('hendleContextMenuClick', data, ev) 17 | const { onClose } = data 18 | onClose && onClose() 19 | } 20 | 21 | // 模板中使用slot.default可以拿到contextmenu中的值,包含visible, x, y, item,以及onClose,可以点击后关闭菜单 22 | 23 | 29 | 30 | ``` 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "outDir": "lib", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "module": "esnext", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "importHelpers": true, 11 | "moduleResolution": "node", 12 | "experimentalDecorators": true, 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "baseUrl": ".", 17 | "types": [ 18 | "webpack-env" 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ], 24 | "@antv/graphin/*": [ 25 | "node_modules/@antv/graphin/*" 26 | ] 27 | }, 28 | "lib": [ 29 | "esnext", 30 | "dom", 31 | "dom.iterable", 32 | "scripthost" 33 | ] 34 | }, 35 | "include": [ 36 | "src/**/*.ts", 37 | "src/**/*.tsx", 38 | "src/**/*.vue", 39 | "tests/**/*.ts", 40 | "tests/**/*.tsx" 41 | ], 42 | "exclude": [ 43 | "node_modules" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /src/behaviors/DragNode.ts: -------------------------------------------------------------------------------- 1 | import useBehaviorHook, { BehaviorComponent } from './useBehaviorHook' 2 | 3 | const defaultConfig = { 4 | /** 5 | * @description 是否禁用该功能 6 | * @default false 7 | */ 8 | disabled: false, 9 | /** 10 | * @description 是否在拖拽节点时更新所有与之相连的边,默认为 true 11 | * @default true 12 | */ 13 | updateEdge: true, 14 | /** 15 | * @description 节点拖拽时的绘图属性 16 | * @default { strokeOpacity: 0.6, fillOpacity: 0.6 } 17 | */ 18 | delegateStyle: {}, 19 | /** 20 | * @description 是否开启delegate 21 | * @default false 22 | */ 23 | enableDelegate: false, 24 | /** 25 | * @description 拖动节点过程中是否只改变 Combo 的大小,而不改变其结构 26 | * @default false 27 | */ 28 | onlyChangeComboSize: false, 29 | /** 30 | * @description 拖动过程中目标 combo 状态样式 31 | * @default '' 32 | */ 33 | comboActiveState: '', 34 | /** 35 | * @description 选中样式 36 | * @default selected 37 | */ 38 | selectedState: 'selected' 39 | } 40 | export const DragNode: BehaviorComponent = useBehaviorHook({ 41 | name: 'DragNode', 42 | type: 'drag-node', 43 | defaultConfig 44 | }) 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Lloyd Zhou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/behaviors/LassoSelect.ts: -------------------------------------------------------------------------------- 1 | import useBehaviorHook, { BehaviorComponent } from './useBehaviorHook' 2 | 3 | const DEFAULT_TRIGGER = 'shift' 4 | const defaultConfig = { 5 | /** 是否禁用该功能 */ 6 | disabled: false, 7 | /** 拖动框选框的样式,包括 fill、fillOpacity、stroke 和 lineWidth 四个属性; */ 8 | delegateStyle: { 9 | fill: '#EEF6FF', 10 | fillOpacity: 0.4, 11 | stroke: '#81A7F8', 12 | lineWidth: 1, 13 | lineDash: [2, 4] 14 | }, 15 | /** 选中节点时的回调,参数 nodes 表示选中的节点; */ 16 | onSelect: () => { 17 | console.debug('onSelect') 18 | }, 19 | /** 取消选中节点时的回调,参数 nodes 表示取消选中的节点; */ 20 | onDeselect: () => { 21 | console.debug('onDeselect') 22 | }, 23 | /** 选中的状态,默认值为 'selected' */ 24 | selectedState: 'selected', 25 | /** 触发框选的动作,默认为 'shift',即用户按住 Shift 键拖动就可以进行框选操作,可配置的的选项为: 'shift'、'ctrl' / 'control'、'alt' 和 'drag' ,不区分大小写 */ 26 | trigger: DEFAULT_TRIGGER, 27 | /** 框选过程中是否选中边,默认为 true,用户配置为 false 时,则不选中边; */ 28 | includeEdges: true 29 | } 30 | export type LassoSelectProps = typeof defaultConfig 31 | export const LassoSelect: BehaviorComponent = useBehaviorHook({ 32 | name: 'LassoSelect', 33 | type: 'lasso-select', 34 | defaultConfig 35 | }) 36 | -------------------------------------------------------------------------------- /src/behaviors/ActivateRelations.ts: -------------------------------------------------------------------------------- 1 | import useBehaviorHook, { BehaviorComponent } from './useBehaviorHook' 2 | 3 | const defaultConfig = { 4 | /** 5 | * @description 是否禁用该功能 6 | * @default false 7 | */ 8 | disabled: false, 9 | /** 10 | * @description 可以是 mousenter,表示鼠标移入时触发;也可以是 click,鼠标点击时触发 11 | * @default mouseenter 12 | */ 13 | trigger: 'click', 14 | /** 15 | * @description 活跃节点状态。当行为被触发,需要被突出显示的节点和边都会附带此状态,默认值为 active;可以与 graph 实例的 nodeStyle 和 edgeStyle 结合实现丰富的视觉效果。 16 | * @default active 17 | */ 18 | activeState: 'active', 19 | /** 20 | * @description 非活跃节点状态。不需要被突出显示的节点和边都会附带此状态。默认值为 inactive。可以与 graph 实例的 nodeStyle 和 edgeStyle 结合实现丰富的视觉效果; 21 | * @default inactive 22 | */ 23 | inactiveState: 'inactive', 24 | /** 25 | * @description 高亮相连节点时是否重置已经选中的节点,默认为 false,即选中的节点状态不会被 activate-relations 覆盖; 26 | * @default false 27 | */ 28 | resetSelected: false 29 | } 30 | export type ActivateRelationsProps = typeof defaultConfig 31 | export const ActivateRelations: BehaviorComponent = useBehaviorHook({ 32 | name: 'ActivateRelations', 33 | type: 'activate-relations', 34 | defaultConfig 35 | }) 36 | -------------------------------------------------------------------------------- /src/behaviors/ZoomCanvas.ts: -------------------------------------------------------------------------------- 1 | import useBehaviorHook, { BehaviorComponent } from './useBehaviorHook' 2 | 3 | const defaultConfig = { 4 | /** 缩放灵敏度,支持 1-10 的数值,默认灵敏度为 5; */ 5 | sensitivity: 2, 6 | /** 最小缩放比例 */ 7 | minZoom: undefined, 8 | /** 最大缩放比例 */ 9 | maxZoom: undefined, 10 | /** 是否开启性能优化,默认为 false,设置为 true 开启,开启后缩放比例小于 optimizeZoom 时自动隐藏非 keyShape */ 11 | enableOptimize: false, 12 | /** 当 enableOptimize 为 true 时起作用,默认值为 0.7,表示当缩放到哪个比例时开始隐藏非 keyShape; */ 13 | optimizeZoom: 0.1, 14 | /** 在缩小画布时是否固定选定元素的描边粗细、文本大小、整体大小等,fixSelectedItems 是一个对象,有以下变量: */ 15 | fixSelectedItems: { 16 | /** 固定元素的整体大小,优先级高于 fixSelectedItems.fixLineWidth 和 fixSelectedItems.fixLabel; */ 17 | fixAll: false, 18 | /** 固定元素的 keyShape 的描边粗细; */ 19 | fixLineWidth: false, 20 | /** 固定元素的文本大小。 */ 21 | fixLabel: false, 22 | /** 将被固定的元素状态,被设置为该状态的节点将会在画布缩小时参与固定大小的计算,默认为 'selected'; */ 23 | fixState: 'selected' 24 | }, 25 | /** 是否禁用该功能 */ 26 | disabled: false 27 | } 28 | export type ZoomCanvasProps = typeof defaultConfig 29 | export const ZoomCanvas: BehaviorComponent = useBehaviorHook({ 30 | name: 'ZoomCanvas', 31 | type: 'zoom-canvas', 32 | defaultConfig, 33 | }) 34 | -------------------------------------------------------------------------------- /src/behaviors/BrushSelect.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import useBehaviorHook, { BehaviorComponent } from './useBehaviorHook' 3 | 4 | const DEFAULT_TRIGGER = 'shift' 5 | const defaultConfig = { 6 | /** 是否禁用该功能 */ 7 | disabled: false, 8 | /** 拖动框选框的样式,包括 fill、fillOpacity、stroke 和 lineWidth 四个属性; */ 9 | brushStyle: { 10 | fill: '#EEF6FF', 11 | fillOpacity: 0.4, 12 | stroke: '#81A7F8', 13 | lineWidth: 1, 14 | lineDash: [2, 4] 15 | }, 16 | /** 选中节点时的回调,参数 nodes 表示选中的节点; */ 17 | onSelect: () => { 18 | console.debug('onSelect') 19 | }, 20 | /** 取消选中节点时的回调,参数 nodes 表示取消选中的节点; */ 21 | onDeselect: () => { 22 | console.debug('onDeselect') 23 | }, 24 | /** 选中的状态,默认值为 'selected' */ 25 | selectedState: 'selected', 26 | /** 触发框选的动作,默认为 'shift',即用户按住 Shift 键拖动就可以进行框选操作,可配置的的选项为: 'shift'、'ctrl' / 'control'、'alt' 和 'drag' ,不区分大小写 */ 27 | trigger: DEFAULT_TRIGGER, 28 | /** 框选过程中是否选中边,默认为 true,用户配置为 false 时,则不选中边; */ 29 | includeEdges: true 30 | } 31 | 32 | export type BrushSelectProps = typeof defaultConfig 33 | export const BrushSelect: BehaviorComponent = useBehaviorHook({ 34 | name: 'BrushSelect', 35 | type: 'brush-select', 36 | defaultConfig 37 | }) 38 | -------------------------------------------------------------------------------- /src/components/Legend/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { defineComponent, CSSProperties, ref, watchEffect, toRaw, h, DefineComponent } from 'vue'; 3 | import type { LegendProps } from './typing'; 4 | import useLegend from './useLegend'; 5 | import { useContext, contextSymbol } from '../../GraphinContext' 6 | import type LegendProps from './typing' 7 | 8 | const defaultStyle: CSSProperties = { 9 | position: 'absolute', 10 | top: '0px', 11 | right: '0px', 12 | }; 13 | 14 | export const Legend: DefineComponent = defineComponent({ 15 | name: 'Legend', 16 | props: ['bindType', 'sortKey', 'style'], 17 | setup(props, { slots }) { 18 | const { bindType = 'node', sortKey, style } = props; 19 | const legend = useLegend({ bindType, sortKey }) 20 | return () => { 21 | const { dataMap, options=[] } = legend 22 | return h('div', { 23 | class: 'graphin-components-legend', 24 | style: { ...defaultStyle, ...style }, 25 | }, slots.default && slots.default({ 26 | bindType, 27 | sortKey, 28 | dataMap, 29 | options, 30 | })) 31 | } 32 | } 33 | }) 34 | 35 | export * from './Node'; 36 | export * from './useLegend'; 37 | 38 | export default Legend 39 | 40 | -------------------------------------------------------------------------------- /src/behaviors/useBehaviorHook.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { useContext, contextSymbol } from '../GraphinContext' 3 | import { defineComponent, onMounted, onUnmounted, DefineComponent } from 'vue' 4 | 5 | export type BehaviorComponent = DefineComponent 6 | 7 | function useBehaviorHook(params: {defaultConfig: T, name: string, type: string}): DefineComponent { 8 | return defineComponent({ 9 | name: params.name, 10 | inject: [contextSymbol], 11 | setup (props, context) { 12 | const { 13 | type, 14 | defaultConfig, 15 | mode = "default" 16 | } = params 17 | const { graph } = useContext() 18 | const { disabled, ...otherConfig } = context.attrs 19 | 20 | onMounted(() => { 21 | /** 保持单例 */ 22 | graph!.removeBehaviors(type, mode); 23 | if (disabled) { 24 | return 25 | } 26 | const config = { 27 | ...defaultConfig, 28 | ...otherConfig 29 | } 30 | graph!.addBehaviors({ 31 | type, 32 | ...config 33 | }, mode) 34 | }) 35 | onUnmounted(() => { 36 | if (!graph.destroyed) { 37 | graph!.removeBehaviors(type, mode) 38 | } 39 | }) 40 | return () => null 41 | } 42 | }) 43 | } 44 | 45 | export default useBehaviorHook 46 | -------------------------------------------------------------------------------- /src/behaviors/FitView.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { defineComponent, onMounted, onUnmounted, DefineComponent } from 'vue' 3 | import { useContext, contextSymbol } from '../GraphinContext' 4 | 5 | export const FitView: DefineComponent<{padding: Array, isBindLayoutChange: boolean}> = defineComponent({ 6 | name: 'FitView', 7 | props: { 8 | padding: { 9 | type: Array, 10 | default: () => [], 11 | }, 12 | isBindLayoutChange: { 13 | type: Boolean, 14 | default: true 15 | } 16 | }, 17 | inject: [contextSymbol], 18 | setup (props) { 19 | const { padding, isBindLayoutChange } = props 20 | const { graph } = useContext() 21 | const handleFitView = () => { 22 | /* afterlayout事件触发后,还需要等refreshPisitions完成再执行fitView */ 23 | setTimeout(() => { 24 | const nodeSize = graph.getNodes().length 25 | if (nodeSize > 0) { 26 | graph.fitView(padding) 27 | } 28 | }, 60) 29 | } 30 | onMounted(() => { 31 | /** 第一次就执行 FitView */ 32 | handleFitView() 33 | if (isBindLayoutChange) { 34 | graph.on('afterlayout', handleFitView) 35 | } 36 | }) 37 | onUnmounted(() => { 38 | if (isBindLayoutChange) { 39 | graph.off('afterlayout', handleFitView) 40 | } 41 | }) 42 | return () => null 43 | } 44 | }) 45 | 46 | -------------------------------------------------------------------------------- /src/behaviors/ResizeCanvas.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { onMounted, onUnmounted, defineComponent, DefineComponent } from 'vue' 3 | import { debounce } from '@antv/util' 4 | import { useContext, contextSymbol } from '../GraphinContext' 5 | 6 | export const ResizeCanvas: DefineComponent<{graphDOM: HTMLDivElement}> = defineComponent({ 7 | name: 'ResizeCanvas', 8 | props: { 9 | graphDOM: { 10 | type: HTMLDivElement, 11 | default: () => ({} as HTMLDivElement), 12 | } 13 | }, 14 | inject: [contextSymbol], 15 | setup (props) { 16 | const { graphDOM } = props 17 | const { graph } = useContext() 18 | 19 | const handleResizeEvent = debounce(() => { 20 | const { 21 | clientWidth, 22 | clientHeight 23 | } = graphDOM 24 | graph!.set('width', clientWidth) 25 | graph!.set('height', clientHeight) 26 | const canvas = graph!.get('canvas') 27 | if (canvas) { 28 | canvas.changeSize(clientWidth, clientHeight) 29 | graph!.autoPaint() 30 | } 31 | }, 200) 32 | 33 | onMounted(() => { 34 | /** 先执行一次 */ 35 | handleResizeEvent() 36 | /** 内置 drag force node */ 37 | window.addEventListener('resize', handleResizeEvent, false) 38 | }) 39 | onUnmounted(() => { 40 | window.removeEventListener('resize', handleResizeEvent, false) 41 | }) 42 | return () => null 43 | } 44 | }) 45 | 46 | -------------------------------------------------------------------------------- /src/components/SnapLine/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import G6 from '@antv/g6'; 3 | import { useContext, contextSymbol } from '../../GraphinContext'; 4 | import { defineComponent, onMounted, onUnmounted, watchEffect, DefineComponent } from 'vue'; 5 | 6 | const defaultOptions = { 7 | line: { 8 | stroke: '#FA8C16', 9 | lineWidth: 0.5, 10 | }, 11 | itemAlignType: 'center', 12 | }; 13 | export interface SnapLineProps { 14 | /** 15 | * @description 是否开启 16 | * @default false 17 | */ 18 | visible: boolean; 19 | /** 20 | * @description 配置项 21 | * @default `https://github.com/antvis/G6/blob/master/packages/plugin/src/snapline/index.ts` 22 | */ 23 | options?: Partial; 24 | } 25 | 26 | export const SnapLine: DefineComponent = defineComponent({ 27 | name: 'SnapLine', 28 | props: { 29 | visible: { 30 | type: Boolean, 31 | default: () => false, 32 | }, 33 | options: { 34 | // type: typeof defaultOptions, 35 | type: Object, 36 | default: () => defaultOptions, 37 | } 38 | }, 39 | inject: [contextSymbol], 40 | setup(props) { 41 | const { graph } = useContext(); 42 | const { options, visible } = props; 43 | watchEffect((onInvalidate) => { 44 | const Options = { 45 | ...defaultOptions, 46 | ...options, 47 | } 48 | const snapLine = new G6.SnapLine(Options as any); 49 | 50 | if (visible) { 51 | graph.addPlugin(snapLine); 52 | } 53 | 54 | onInvalidate(() => { 55 | if (graph && !graph.destroyed) { 56 | graph.removePlugin(snapLine); 57 | } 58 | }) 59 | }) 60 | return () => null 61 | } 62 | }) 63 | 64 | export default SnapLine; 65 | -------------------------------------------------------------------------------- /src/behaviors/Hoverable.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { onMounted, onUnmounted, defineComponent, DefineComponent } from 'vue' 3 | import { useContext, contextSymbol } from '../GraphinContext' 4 | 5 | export const Hoverable: DefineComponent<{bindType: string}> = defineComponent({ 6 | name: 'Hoverable', 7 | props: { 8 | bindType: { 9 | type: String 10 | } 11 | }, 12 | inject: [contextSymbol], 13 | setup (props) { 14 | const { bindType = 'node' } = props 15 | const { graph } = useContext() 16 | 17 | const handleNodeMouseEnter = (evt) => { 18 | graph.setItemState(evt.item, 'hover', true) 19 | } 20 | const handleNodeMouseLeave = (evt) => { 21 | graph.setItemState(evt.item, 'hover', false) 22 | } 23 | const handleEdgeMouseEnter = (evt) => { 24 | graph.setItemState(evt.item, 'hover', true) 25 | } 26 | const handleEdgeMouseLeave = (evt) => { 27 | graph.setItemState(evt.item, 'hover', false) 28 | } 29 | 30 | onMounted(() => { 31 | if (bindType === 'node') { 32 | graph.on('node:mouseenter', handleNodeMouseEnter) 33 | graph.on('node:mouseleave', handleNodeMouseLeave) 34 | } 35 | if (bindType === 'edge') { 36 | graph.on('edge:mouseenter', handleEdgeMouseEnter) 37 | graph.on('edge:mouseleave', handleEdgeMouseLeave) 38 | } 39 | }) 40 | onUnmounted(() => { 41 | if (bindType === 'node') { 42 | graph.off('node:mouseenter', handleNodeMouseEnter) 43 | graph.off('node:mouseleave', handleNodeMouseLeave) 44 | } 45 | if (bindType === 'edge') { 46 | graph.off('edge:mouseenter', handleEdgeMouseEnter) 47 | graph.off('edge:mouseleave', handleEdgeMouseLeave) 48 | } 49 | }) 50 | 51 | return () => null 52 | } 53 | }) 54 | 55 | -------------------------------------------------------------------------------- /src/behaviors/DragNodeWithForce.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { onMounted, onUnmounted, defineComponent, DefineComponent } from 'vue' 3 | import { useContext, contextSymbol } from '../GraphinContext' 4 | 5 | export const DragNodeWithForce: DefineComponent<{autoPin: boolean}> = defineComponent({ 6 | name: 'DragNodeWithForce', 7 | props: { 8 | autoPin: { 9 | type: Boolean, 10 | default: () => true, 11 | } 12 | }, 13 | inject: [contextSymbol], 14 | setup (props) { 15 | const { 16 | graph, 17 | layout 18 | } = useContext() 19 | const { autoPin } = props 20 | const handleNodeDragStart = () => { 21 | const { instance = {} } = layout 22 | const { simulation } = instance 23 | if (simulation) { 24 | simulation.stop() 25 | } 26 | } 27 | const handleNodeDragEnd = (e) => { 28 | const { instance = {} } = layout 29 | const { 30 | simulation, 31 | type 32 | } = instance 33 | if (type !== 'graphin-force') { 34 | return 35 | } 36 | if (e.item) { 37 | console.log('e.item', instance) 38 | const nodeModel = e.item.get('model') 39 | nodeModel.x = e.x 40 | nodeModel.y = e.y 41 | nodeModel.layout = { 42 | ...nodeModel.layout, 43 | force: { 44 | mass: autoPin ? 1000000 : null 45 | } 46 | } 47 | const drageNodes = [nodeModel] 48 | simulation.restart(drageNodes, graph) 49 | graph.refreshPositions() 50 | } 51 | } 52 | onMounted(() => { 53 | graph.on('node:dragstart', handleNodeDragStart) 54 | graph.on('node:dragend', handleNodeDragEnd) 55 | }) 56 | onUnmounted(() => { 57 | graph.off('node:dragstart', handleNodeDragStart) 58 | graph.off('node:dragend', handleNodeDragEnd) 59 | }) 60 | return () => null 61 | } 62 | }) 63 | 64 | -------------------------------------------------------------------------------- /src/components/ContextMenu/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { defineComponent, CSSProperties, ref, h, DefineComponent } from 'vue'; 3 | import '@antv/graphin/es/components/ContextMenu/index.css' 4 | 5 | import { useContextMenu, ContextMenuProps } from './useContextMenu' 6 | 7 | const defaultStyle: CSSProperties = { 8 | width: '120px', 9 | boxShadow: '0 4px 12px rgb(0 0 0 / 15%)', 10 | }; 11 | 12 | export const ContextMenu: DefineComponent = defineComponent({ 13 | name: 'ContextMenu', 14 | props: { 15 | bindType: { 16 | type: String, 17 | default: () => 'node' 18 | }, 19 | bindEvent: { 20 | type: String, 21 | default: () => 'contextmenu' 22 | }, 23 | style: { 24 | type: Object, 25 | default: () => ({}) 26 | }, 27 | }, 28 | setup(props, { slots }) { 29 | const { bindType, bindEvent } = props; 30 | const container = ref(null); 31 | const contextmenu = useContextMenu({ 32 | bindType, 33 | bindEvent, 34 | container, 35 | }); 36 | 37 | return () => { 38 | const { visible, x, y, item, onClose, selectedItems } = contextmenu 39 | const { style } = props 40 | 41 | const positionStyle: CSSProperties = { 42 | position: 'absolute', 43 | left: x + 'px', 44 | top: y + 'px', 45 | }; 46 | 47 | const id = (item && !item.destroyed && item.getModel && item.getModel().id) || ''; 48 | 49 | return h('div', { 50 | ref: container, 51 | className: "graphin-components-contextmenu", 52 | style: { ...defaultStyle, ...style, ...positionStyle }, 53 | key: id, 54 | }, visible && slots.default ? slots.default({ 55 | visible, x, y, item, onClose, 56 | id, 57 | selectedItems, 58 | }) : null); 59 | } 60 | } 61 | }) 62 | 63 | export * from './useContextMenu' 64 | export default ContextMenu; 65 | 66 | -------------------------------------------------------------------------------- /src/behaviors/TreeCollapse.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { onMounted, onUnmounted, defineComponent, DefineComponent } from 'vue' 3 | import { useContext, contextSymbol } from '../GraphinContext' 4 | 5 | const defaultConfig = { 6 | /** 收起和展开树图的方式,支持 'click' 和 'dblclick' 两种方式。默认为 'click',即单击; */ 7 | trigger: 'click' 8 | /** 9 | * 收起或展开的回调函数。 10 | * 警告:G6 V3.1.2 版本中将移除;itemcollapsed:当 collapse-expand 发生时被触发。 11 | * 请使用 graph.on('itemcollapsed', e => {...}) 监听,参数 e 有以下字段: 12 | * */ 13 | } 14 | const type = 'collapse-expand' 15 | const mode = 'default' 16 | 17 | export type TreeCollapseProps = {disabled: boolean, onChange: any } & typeof defaultConfig 18 | export const TreeCollapse: DefineComponent = defineComponent({ 19 | name: 'TreeCollapse', 20 | props: { 21 | disabled: { 22 | type: Boolean 23 | }, 24 | onChange: { 25 | type: Function 26 | } 27 | }, 28 | inject: [contextSymbol], 29 | setup (props, context) { 30 | const { 31 | disabled, 32 | onChange 33 | } = props 34 | const { ...otherConfig } = context 35 | const { graph } = useContext() 36 | 37 | const handleChange = (e) => { 38 | const { 39 | item, 40 | collapsed 41 | } = e 42 | const model = item.get('model') 43 | model.collapsed = collapsed 44 | if (onChange) { 45 | onChange(item, collapsed) // callback 46 | } 47 | } 48 | onMounted(() => { 49 | graph.removeBehaviors(type, mode) 50 | if (disabled) { 51 | return 52 | } 53 | graph.addBehaviors({ 54 | type, 55 | ...defaultConfig, 56 | ...otherConfig 57 | }, mode) 58 | graph.on('itemcollapsed', handleChange) 59 | }) 60 | onUnmounted(() => { 61 | graph.off('itemcollapsed', handleChange) 62 | if (!graph.destroyed) { 63 | graph.removeBehaviors(type, mode) 64 | } 65 | }) 66 | return () => null 67 | } 68 | }) 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphin-vue 2 | 3 | NPM Package 4 | NPM Size 5 | NPM Downloads 6 | MIT License 7 | 8 | ## 核心思想 9 | 1. 直接使用@antv/graphin内置的shape和layout逻辑代码 10 | > 使用一个移除react依赖的@antv/grapin核心库(详情[antvis/Graphin#370](https://github.com/antvis/Graphin/pull/370)) 11 | > 可以在不依赖react的情况下使用graphin内置的shape和layout代码 12 | 13 | 2. 使用vue重写ui组件以及behaviors组件以及components组件 14 | 15 | ## Install 16 | ``` 17 | yarn add antv-graphin-vue @antv/graphin 18 | ``` 19 | 20 | ## Example 21 | 22 | [online demo](https://codesandbox.io/s/graphin-vue-demo-460uf7) 23 | 24 | 25 | 这个是使用jsx实现的vue版本的示例 26 | ``` 27 | import { defineComponent, reactive } from 'vue' 28 | import Graphin, { Utils, Behaviors, Components } from 'antv-graphin-vue' 29 | const { DragCanvas, ZoomCanvas, DragNode, ResizeCanvas } = Behaviors 30 | const { MiniMap } = Components 31 | 32 | const App = defineComponent({ 33 | components: { Graphin, DragCanvas, ZoomCanvas, DragNode, ResizeCanvas, MiniMap }, 34 | setup(props) { 35 | const state = reactive({ 36 | data: {}, 37 | layout: { 38 | type: 'force' 39 | } 40 | }) 41 | onMounted(() => { 42 | state.data = Utils.mock(10).circle().graphin() 43 | }) 44 | return () => ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ) 53 | } 54 | }) 55 | 56 | export default App 57 | 58 | ``` 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/registers.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import type { BehaviorOption, ShapeDefine, ShapeOptions } from '@antv/g6'; 3 | import G6 from '@antv/g6'; 4 | import type { RegisterFunction } from './Graphin'; 5 | import type { IconLoader } from './typings/type'; 6 | 7 | export const registerNode = ( 8 | shapeType: string, 9 | options: ShapeOptions | ShapeDefine, 10 | extendShapeType?: string, 11 | ): RegisterFunction => { 12 | return G6.registerNode(shapeType, options, extendShapeType); 13 | }; 14 | 15 | export const registerEdge = ( 16 | edgeName: string, 17 | options, 18 | extendedEdgeName: string, 19 | ): RegisterFunction => { 20 | return G6.registerEdge(edgeName, options, extendedEdgeName); 21 | }; 22 | 23 | export const registerCombo = (comboName, options, extendedComboName): RegisterFunction => { 24 | return G6.registerCombo(comboName, options, extendedComboName); 25 | }; 26 | export const registerBehavior = ( 27 | behaviorName: string, 28 | behavior: BehaviorOption, 29 | ): RegisterFunction => { 30 | // @ts-ignore 31 | return G6.registerBehavior(behaviorName, behavior); 32 | }; 33 | 34 | export const registerFontFamily = (iconLoader: IconLoader): Record => { 35 | /** 注册 font icon */ 36 | const iconFont = iconLoader(); 37 | const { glyphs, fontFamily } = iconFont; 38 | const icons = glyphs.map((item) => { 39 | return { 40 | name: item.name, 41 | unicode: String.fromCodePoint(item.unicode_decimal), 42 | }; 43 | }); 44 | 45 | return new Proxy(icons, { 46 | get: (target, propKey: string) => { 47 | const matchIcon = target.find((icon) => { 48 | return icon.name === propKey; 49 | }); 50 | if (!matchIcon) { 51 | console.error(`%c fontFamily:${fontFamily},does not found ${propKey} icon`); 52 | return ''; 53 | } 54 | return matchIcon ? matchIcon.unicode : undefined; 55 | }, 56 | }); 57 | }; 58 | 59 | export const registerLayout = function (layoutName: string, layout: any) { 60 | return G6.registerLayout(layoutName, layout); 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/Legend/useLegend.ts: -------------------------------------------------------------------------------- 1 | // @ts-no-check 2 | import { onMounted, onUnmounted, toRefs, shallowReactive, ref } from 'vue' 3 | import { IG6GraphEvent } from '@antv/g6' 4 | import { useContext } from '../../GraphinContext'; 5 | import { LegendProps } from './typing' 6 | import { getEnumValue, getEnumDataMap } from '@antv/graphin/es/utils/processGraphData'; 7 | 8 | export const useLegend = (props: LegendProps) => { 9 | const { bindType = 'node', sortKey } = props; 10 | // @ts-ignore 11 | const { graph } = useContext(); 12 | const state = shallowReactive({ 13 | dataMap: new Map(), 14 | options: [], 15 | data: {} 16 | }) 17 | const calcDataMap = () => { 18 | const data = graph.save(); 19 | 20 | /** 暂时不支持treeGraph的legend */ 21 | if (data.children) { 22 | console.error('not support tree graph'); 23 | state.dataMap = new Map() 24 | state.options = [] 25 | } else { 26 | // @ts-ignore 27 | const dataMap = getEnumDataMap(data[`${bindType}s`], sortKey); 28 | 29 | /** 计算legend.content 的 options */ 30 | const keys = Array.from(dataMap.keys()); 31 | const options = keys.map(key => { 32 | const item = (dataMap.get(key) || [{}])[0]; 33 | 34 | const graphinStyleColor = getEnumValue(item, 'style.keyshape.fill'); 35 | const g6StyleCcolor = getEnumValue(item, 'style.color'); 36 | 37 | const color = graphinStyleColor || g6StyleCcolor; 38 | return { 39 | /** 颜色 */ 40 | color, 41 | /** 值 */ 42 | value: key, 43 | /** 标签 */ 44 | label: key, 45 | /** 是否选中 */ 46 | checked: true, 47 | }; 48 | }); 49 | state.dataMap = dataMap 50 | // @ts-ignore 51 | state.options = options 52 | } 53 | } 54 | 55 | onMounted(() => { 56 | graph.on('aftergraphrefreshposition', calcDataMap) 57 | }) 58 | onUnmounted(() => { 59 | graph.off('aftergraphrefreshposition', calcDataMap) 60 | }) 61 | 62 | return state 63 | }; 64 | export default useLegend; 65 | 66 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import Graphin from './Graphin' 3 | 4 | //utils 工具 5 | import Utils from "@antv/graphin/es/utils/index" 6 | import * as Behaviors from './behaviors'; 7 | import * as Components from './components'; 8 | import GraphinContext from './GraphinContext'; 9 | 10 | import registerGraphinForce from '@antv/graphin/es/layout/inner/registerGraphinForce'; 11 | import registerPresetLayout from '@antv/graphin/es/layout/inner/registerPresetLayout'; 12 | import { registerGraphinCircle, registerGraphinLine } from '@antv/graphin/es/shape'; 13 | import {registerNode, registerEdge, registerCombo, registerBehavior, registerFontFamily} from "./registers" 14 | // install 是默认的方法。当外界在 use 这个组件的时候,就会调用本身的 install 方法,同时传一个 Vue 这个类的参数。 15 | const components = [ 16 | Graphin, 17 | ] 18 | Graphin.install = function (Vue) { 19 | components.forEach(component => { 20 | Vue.component(component.name, component); 21 | }); 22 | } 23 | 24 | /** 注册 Graphin force 布局 */ 25 | registerGraphinForce(); 26 | /** 注册 Graphin preset 布局 */ 27 | registerPresetLayout(); 28 | 29 | /** 注册 Graphin Circle Node */ 30 | registerGraphinCircle(); 31 | 32 | /** 注册 Graphin line Edge */ 33 | registerGraphinLine(); 34 | 35 | /* istanbul ignore if */ 36 | if (typeof window !== 'undefined' && window.Vue) { 37 | Graphin.install(window.Vue); 38 | } 39 | 40 | //导出 41 | export { 42 | Graphin, 43 | Utils, 44 | Behaviors, 45 | Components, 46 | GraphinContext, 47 | registerNode, 48 | registerEdge, 49 | registerCombo, 50 | registerBehavior, 51 | registerFontFamily 52 | } 53 | 54 | export { 55 | /** export G6 */ 56 | default as G6, 57 | /** export G6 Type */ 58 | // Graph, 59 | // IG6GraphEvent, 60 | // GraphData, 61 | // TreeGraphData, 62 | // NodeConfig, 63 | // EdgeConfig, 64 | } from '@antv/g6'; 65 | 66 | export interface GraphEvent extends MouseEvent { 67 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 68 | item: any; 69 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 70 | target: any; 71 | } 72 | export default Graphin 73 | 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "antv-graphin-vue", 3 | "version": "2.6.13", 4 | "description": "the vue toolkit for graph analysis based on g6, based on @antv/graphin", 5 | "scripts": { 6 | "prepare": "install-peers", 7 | "serve": "tsc -w & vue-cli-service serve example/main.ts", 8 | "build": "rm -rf lib && tsc -p tsconfig.porduction.json", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/lloydzhou/graphin-vue.git" 14 | }, 15 | "files": [ 16 | "lib" 17 | ], 18 | "main": "lib/index.js", 19 | "module": "lib/index.js", 20 | "peerDependencies": { 21 | "@antv/g6": "^4.3.4", 22 | "core-js": "^3.6.5", 23 | "lodash": "^4.17.21", 24 | "vue": "^3.2.31" 25 | }, 26 | "dependencies": { 27 | "@antv/graphin": "^2.6.8" 28 | }, 29 | "devDependencies": { 30 | "@antv/graphin-icons": "^1.0.0", 31 | "@typescript-eslint/eslint-plugin": "^4.18.0", 32 | "@typescript-eslint/parser": "^4.18.0", 33 | "@vue/babel-plugin-jsx": "^1.1.1", 34 | "@vue/cli-plugin-babel": "~4.5.11", 35 | "@vue/cli-plugin-eslint": "~4.5.11", 36 | "@vue/cli-plugin-typescript": "~4.5.11", 37 | "@vue/cli-service": "~4.5.11", 38 | "@vue/compiler-sfc": "^3.2.31", 39 | "@vue/eslint-config-typescript": "^7.0.0", 40 | "acorn-jsx": "^5.3.2", 41 | "ant-design-vue": "^3.1.1", 42 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 43 | "babel-plugin-lodash": "^3.3.4", 44 | "babel-plugin-syntax-jsx": "^6.18.0", 45 | "babel-plugin-transform-vue-jsx": "^3.7.0", 46 | "eslint": "^6.7.2", 47 | "eslint-plugin-vue": "^7.0.0", 48 | "install-peers-cli": "^2.2.0", 49 | "less": "^4.1.2", 50 | "less-loader": "7.3.0", 51 | "lint-staged": "^9.5.0", 52 | "typescript": "~4.1.5", 53 | "vue": "^3.2.31" 54 | }, 55 | "author": "lloydzhou", 56 | "license": "MIT", 57 | "publishConfig": { 58 | "access": "public" 59 | }, 60 | "eslintConfig": { 61 | "root": true, 62 | "env": { 63 | "node": true 64 | }, 65 | "extends": [ 66 | "plugin:vue/vue3-essential", 67 | "eslint:recommended", 68 | "@vue/typescript/recommended" 69 | ], 70 | "parserOptions": { 71 | "ecmaVersion": 2020 72 | }, 73 | "rules": { 74 | "@typescript-eslint/ban-ts-comment": "off", 75 | "@typescript-eslint/no-var-requires": "error", 76 | "vue/no-setup-props-destructure": "off" 77 | } 78 | }, 79 | "browserslist": [ 80 | "> 1%", 81 | "last 2 versions", 82 | "not dead" 83 | ], 84 | "gitHooks": { 85 | "pre-commit": "lint-staged" 86 | }, 87 | "lint-staged": { 88 | "*.{js,jsx,vue,ts,tsx}": [ 89 | "vue-cli-service lint", 90 | "git add" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/components/Tooltip/getContainerStyles.ts: -------------------------------------------------------------------------------- 1 | import type { TooltipProps } from './index'; 2 | 3 | export const getContainerStyles = ({ 4 | placement, 5 | nodeSize, 6 | x, 7 | y, 8 | bindType = 'node', 9 | visible, 10 | }: { 11 | visible: boolean; 12 | placement: TooltipProps['placement']; 13 | nodeSize: number; 14 | x: number; 15 | y: number; 16 | bindType: string; 17 | }) => { 18 | if (bindType === 'edge') { 19 | return { 20 | left: x, 21 | top: y, 22 | }; 23 | } 24 | 25 | if (placement === 'top') { 26 | if (visible) { 27 | return { 28 | left: x, 29 | top: y - nodeSize / 2, 30 | opacity: 1, 31 | transform: 'translate(-50%,calc(-100% - 6px))', 32 | transition: 'opacity 0.5s,transform 0.5s', 33 | }; 34 | } 35 | return { 36 | left: 0, 37 | top: 0, 38 | opacity: 0.5, 39 | transform: 'translate(-50%,-100%)', 40 | transition: 'opacity 0.5s,transform 0.5s', 41 | }; 42 | } 43 | if (placement === 'bottom') { 44 | if (visible) { 45 | return { 46 | left: x, 47 | top: y + nodeSize / 2, 48 | opacity: 1, 49 | transform: 'translate(-50%,6px)', 50 | transition: 'opacity 0.5s,transform 0.5s', 51 | }; 52 | } 53 | return { 54 | left: x, 55 | top: y + nodeSize / 2, 56 | opacity: 0.5, 57 | transform: 'translate(-50%,0px)', 58 | transition: 'opacity 0.5s,transform 0.5s', 59 | }; 60 | } 61 | if (placement === 'left') { 62 | if (visible) { 63 | return { 64 | left: x - nodeSize / 2, 65 | top: y, 66 | transform: 'translate(calc(-100% - 6px),-50%)', 67 | opacity: 1, 68 | transition: 'opacity 0.5s,transform 0.5s', 69 | }; 70 | } 71 | return { 72 | opacity: 0, 73 | left: x - nodeSize / 2, 74 | top: y, 75 | transform: 'translate(-100%,-50%)', 76 | transition: 'opacity 0.5s,transform 0.5s', 77 | }; 78 | } 79 | if (placement === 'right') { 80 | if (visible) { 81 | return { 82 | left: x + nodeSize / 2, 83 | top: y, 84 | transform: 'translate(6px,-50%)', 85 | opacity: 1, 86 | transition: 'opacity 0.5s,transform 0.5s', 87 | }; 88 | } 89 | return { 90 | left: x + nodeSize / 2, 91 | top: y, 92 | transform: 'translate(0,-50%)', 93 | opacity: 0, 94 | transition: 'opacity 0.5s,transform 0.5s', 95 | }; 96 | } 97 | if (placement === 'center') { 98 | if (visible) { 99 | return { 100 | left: x, 101 | top: y, 102 | opacity: 1, 103 | transition: 'opacity 0.5s,transform 0.5s', 104 | }; 105 | } 106 | return { 107 | left: x, 108 | top: y, 109 | opacity: 0, 110 | transition: 'opacity 0.5s,transform 0.5s', 111 | }; 112 | } 113 | 114 | return { 115 | left: x, 116 | top: y, 117 | }; 118 | }; 119 | 120 | export default getContainerStyles; 121 | -------------------------------------------------------------------------------- /src/components/Legend/Node.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { defineComponent, watch, shallowReactive, h, Fragment, DefineComponent } from 'vue'; 3 | import '@antv/graphin/es/components/Legend/index.css' 4 | import { useContext } from '../../GraphinContext'; 5 | import deepEqual from '@antv/graphin/es/utils/deepEqual'; 6 | import type LegendChildrenProps from './typing' 7 | 8 | export const LegendNode: DefineComponent = defineComponent({ 9 | name: 'LegendNode', 10 | props: { 11 | options: { 12 | type: Object, 13 | }, 14 | dataMap: { 15 | type: Object, 16 | } 17 | }, 18 | setup(props, { slots }) { 19 | const { graph, theme } = useContext() 20 | const { mode='light' } = theme; 21 | 22 | const state = shallowReactive({ 23 | items: props.options 24 | }) 25 | 26 | watch(() => props.options, (items) => state.items = items) 27 | 28 | const handleClick = (option: OptionType) => { 29 | const checkedValue = { ...option, checked: !option.checked }; 30 | const result = state.items.map((c: any) => { 31 | const matched = c.value === option.value; 32 | return matched ? checkedValue : c; 33 | }); 34 | state.items = result 35 | const nodes = props.dataMap.get(checkedValue.value); 36 | 37 | /** highlight */ 38 | // const nodesId = nodes.map((c) => c.id); 39 | // apis.highlightNodeById(nodesId); 40 | 41 | // @ts-ignore 42 | nodes.forEach((node: any) => { 43 | graph.setItemState(node.id, 'active', checkedValue.checked); 44 | graph.setItemState(node.id, 'inactive', !checkedValue.checked); 45 | }); 46 | }; 47 | 48 | return () => { 49 | return h('ul', {class: 'graphin-components-legend-content'}, state.items.map((option: OptionType, index: number) => { 50 | const { label, checked, color } = option; 51 | const dotColors = { 52 | light: { 53 | active: color, 54 | inactive: '#ddd', 55 | }, 56 | dark: { 57 | active: color, 58 | inactive: '#2f2f2f', 59 | }, 60 | }; 61 | const labelColor = { 62 | light: { 63 | active: '#000', 64 | inactive: '#ddd', 65 | }, 66 | dark: { 67 | active: '#fff', 68 | inactive: '#2f2f2f', 69 | }, 70 | }; 71 | const status = checked ? 'active' : 'inactive'; 72 | return h('li', { 73 | key: option.value, 74 | class: 'item', 75 | onClick: () => handleClick(option), 76 | onKeyDown: () => handleClick(option) 77 | }, slots.default ? slots.default({ 78 | dotColor: dotColors[mode][status], 79 | labelColor: labelColor[mode][status], 80 | label, 81 | option, 82 | data: props.dataMap.get(option.value), 83 | }) : h(Fragment, {}, [ 84 | h('span', {class: 'dot', style: {background: dotColors[mode][status] }}), 85 | h('span', {class: 'label', style: {color: labelColor[mode][status] }}, label), 86 | ])) 87 | })) 88 | } 89 | } 90 | }) 91 | 92 | export default LegendNode; 93 | -------------------------------------------------------------------------------- /src/components/FishEye/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import G6 from '@antv/g6'; 3 | import { useContext, contextSymbol } from '../../GraphinContext'; 4 | import { defineComponent, onMounted, onUnmounted, ref, DefineComponent } from 'vue'; 5 | 6 | const defaultOptions = { 7 | r: 249, 8 | scaleRByWheel: true, 9 | minR: 100, 10 | maxR: 500, 11 | /** 12 | * @description 放大镜样式 13 | */ 14 | delegateStyle: { 15 | stroke: '#000', 16 | strokeOpacity: 0.8, 17 | lineWidth: 2, 18 | fillOpacity: 0.1, 19 | fill: '#ccc', 20 | }, 21 | showLabel: false, 22 | }; 23 | export interface FishEyeProps { 24 | /** 25 | * @description 是否开启 26 | * @default false 27 | */ 28 | visible: boolean; 29 | /** 30 | * @description FishEye的配置项 31 | * @default { r: 249,scaleRByWheel: true,minR: 100,maxR: 500 } 32 | */ 33 | options?: Partial; 34 | /** 35 | * @description 监听用户按下 ESC 键的回调函数 36 | * @default ()=>{} 37 | */ 38 | handleEscListener?: () => void; 39 | } 40 | 41 | 42 | export const FishEye: DefineComponent = defineComponent({ 43 | name: 'FishEye', 44 | props: { 45 | handleEscListener: { 46 | type: Function as FishEyeProps['handleEscListener'] 47 | }, 48 | visible: { 49 | type: Boolean as FishEyeProps['visible'], 50 | default: () => true, 51 | }, 52 | options: { 53 | type: Object as FishEyeProps['options'], 54 | default: () => ({}) 55 | } 56 | }, 57 | inject: [contextSymbol], 58 | setup(props) { 59 | const { graph } = useContext(); 60 | const { options, visible, handleEscListener } = props; 61 | 62 | const escListener = e => { 63 | if (e.keyCode === 27) { 64 | if (handleEscListener) { 65 | handleEscListener(); 66 | } 67 | graph.get('canvas').setCursor('default'); 68 | } 69 | }; 70 | const fishEye = ref() 71 | onMounted(() => { 72 | const FishEyeOptions = { 73 | ...defaultOptions, 74 | ...options, 75 | }; 76 | 77 | if (FishEyeOptions.showLabel) { 78 | // 先将图上的label全部隐藏 79 | 80 | graph.getNodes().forEach(node => { 81 | node 82 | .getContainer() 83 | .getChildren() 84 | .forEach(shape => { 85 | if (shape.get('type') === 'text') shape.hide(); 86 | }); 87 | }); 88 | } 89 | fishEye.value = new G6.Fisheye(FishEyeOptions); 90 | if (visible) { 91 | graph.addPlugin(fishEye.value); 92 | graph.get('canvas').setCursor('zoom-in'); 93 | } 94 | 95 | if (handleEscListener) { 96 | window.addEventListener('keydown', escListener); 97 | } 98 | }) 99 | onUnmounted(() => { 100 | if (graph && !graph.destroyed) { 101 | graph.get('canvas').setCursor('default'); 102 | graph.removePlugin(fishEye.value); 103 | } 104 | if (handleEscListener) { 105 | window.removeEventListener('keydown', escListener); 106 | } 107 | }) 108 | return () => null 109 | } 110 | }); 111 | 112 | export default FishEye; 113 | 114 | -------------------------------------------------------------------------------- /src/components/MiniMap/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { defineComponent, CSSProperties, onMounted, onUnmounted, ref, h, DefineComponent } from 'vue'; 3 | import G6 from '@antv/g6' 4 | import { useContext, contextSymbol } from '../../GraphinContext' 5 | 6 | 7 | const defaultOptions = { 8 | className: 'graphin-minimap', 9 | viewportClassName: 'graphin-minimap-viewport', 10 | // Minimap 中默认展示和主图一样的内容,KeyShape 只展示节点和边的 key shape 部分,delegate表示展示自定义的rect,用户可自定义样式 11 | type: 'default' as 'default' | 'keyShape' | 'delegate' | undefined, 12 | padding: 50, 13 | size: [200, 120], 14 | delegateStyle: { 15 | fill: '#40a9ff', 16 | stroke: '#096dd9', 17 | }, 18 | refresh: true, 19 | }; 20 | 21 | export interface MiniMapProps { 22 | /** 23 | * @description 是否开启 24 | * @default false 25 | */ 26 | visible: boolean; 27 | /** 28 | * @description MiniMap 配置项 29 | * @default 30 | */ 31 | options?: Partial; 32 | 33 | style?: CSSProperties; 34 | } 35 | 36 | const styles: { 37 | [key: string]: CSSProperties; 38 | } = { 39 | container: { 40 | position: 'absolute', 41 | bottom: 0, 42 | left: 0, 43 | background: '#fff', 44 | boxShadow: 45 | '0px 8px 10px -5px rgba(0,0,0,0.2), 0px 16px 24px 2px rgba(0,0,0,0.14), 0px 6px 30px 5px rgba(0,0,0,0.12)', 46 | }, 47 | }; 48 | 49 | export const MiniMap: DefineComponent = defineComponent({ 50 | name: 'MiniMap', 51 | props: { 52 | style: { 53 | type: Object as MiniMapProps['style'], 54 | default: () => ({}), 55 | }, 56 | visible: { 57 | type: Boolean as MiniMapProps['visible'], 58 | default: () => false, 59 | }, 60 | options: { 61 | type: Object as MiniMapProps['options'], 62 | default: () => ({}) 63 | } 64 | }, 65 | inject: [contextSymbol], 66 | setup(props) { 67 | const { graph } = useContext(); 68 | const { options, style = {} } = props; 69 | const mergedStyle = { 70 | ...styles.container, 71 | ...style, 72 | }; 73 | const miniMap = ref() 74 | // const containerRef: null | HTMLDivElement = null; 75 | const containerRef = ref(); 76 | const containerHeight = 120; 77 | 78 | onMounted(() => { 79 | const width = graph.getWidth(); 80 | const height = graph.getHeight(); 81 | const padding = graph.get('fitViewPadding'); 82 | 83 | const containerSize = [((width - padding * 2) / (height - padding * 2)) * containerHeight, containerHeight]; 84 | 85 | const miniMapOptions = { 86 | container: containerRef.value, 87 | ...defaultOptions, 88 | size: containerSize, 89 | ...options, 90 | }; 91 | 92 | miniMap.value = new G6.Minimap(miniMapOptions); 93 | 94 | graph.addPlugin(miniMap.value); 95 | }) 96 | onUnmounted(() => { 97 | if (miniMap.value && !miniMap.value.destroyed) { 98 | graph.removePlugin(miniMap.value); 99 | } 100 | }) 101 | return () => h('div', { 102 | ref: containerRef, 103 | style: mergedStyle 104 | }) 105 | } 106 | }); 107 | 108 | export default MiniMap; 109 | 110 | -------------------------------------------------------------------------------- /src/components/Tooltip/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { defineComponent, CSSProperties, ref, h, DefineComponent } from 'vue'; 3 | import getContainerStyles from './getContainerStyles'; 4 | import useTooltip from './useTooltip' 5 | import '@antv/graphin/es/components/Tooltip/index.css' 6 | 7 | const defaultStyle: CSSProperties = { 8 | width: '120px', 9 | boxShadow: '0 4px 12px rgb(0 0 0 / 15%)', 10 | }; 11 | 12 | export interface TooltipProps { 13 | /** 14 | * @description tooltip绑定的图元素 15 | * @default node 16 | */ 17 | bindType?: 'node' | 'edge'; 18 | /** 19 | * @description children 20 | * @type React.ReactChild | JSX.Element 21 | */ 22 | // children: (props: TooltipValue) => React.ReactNode; 23 | /** 24 | * @description styles 25 | */ 26 | style?: CSSProperties; 27 | /** 28 | * @description Tooltip 的位置 29 | */ 30 | placement?: 'top' | 'bottom' | 'right' | 'left' | 'center'; 31 | /** 32 | * @description 是否展示小箭头 33 | * @description.en-US display arrow 34 | */ 35 | hasArrow?: boolean; 36 | } 37 | 38 | export const Tooltip: DefineComponent = defineComponent({ 39 | name: 'Tooltip', 40 | props: { 41 | bindType: { 42 | type: String, 43 | default: () => 'node' 44 | }, 45 | style: { 46 | type: Object, 47 | default: () => ({}) 48 | }, 49 | placement: { 50 | // type: 'top' | 'bottom' | 'right' | 'left' | 'center', 51 | type: String, 52 | default: () => 'top' 53 | }, 54 | hasArrow: { 55 | type: Boolean, 56 | default: () => false, 57 | } 58 | }, 59 | setup(props, { slots }) { 60 | const { bindType } = props; 61 | const container = ref(null); 62 | const tooltip = useTooltip({ bindType, container }); 63 | 64 | return () => { 65 | const { visible, x, y, item } = tooltip 66 | const { style, placement='top', hasArrow, bindType='node' } = props 67 | let nodeSize = 40; 68 | 69 | try { 70 | if (item) { 71 | const { type } = item.getModel(); 72 | if (type === 'graphin-cirle') { 73 | const { style } = item.getModel(); 74 | if (style) { 75 | nodeSize = style.keyshape.size as number; 76 | } 77 | } 78 | } 79 | } catch (error) { 80 | console.log(error); 81 | } 82 | const padding = 12; 83 | const containerPosition = getContainerStyles({ placement, nodeSize: nodeSize + padding, x, y, bindType, visible }); 84 | 85 | const positionStyle: CSSProperties = { 86 | position: 'absolute', 87 | ...containerPosition, 88 | left: containerPosition.left + 'px', 89 | top: containerPosition.top + 'px', 90 | }; 91 | 92 | const model = (item && !item.destroyed && item.getModel && item.getModel()) || {}; 93 | const id = model.id || ''; 94 | 95 | return h('div', { 96 | ref: container, 97 | class: `graphin-components-tooltip ${placement}`, 98 | style: { ...defaultStyle, ...style, ...positionStyle }, 99 | key: id, 100 | }, [ 101 | visible && hasArrow ? h('div', {class: `tooltip-arrow ${placement}`}) : null, 102 | visible && slots.default && slots.default({item, bindType, model, id}), 103 | ]) 104 | } 105 | } 106 | }) 107 | 108 | export * from './useTooltip' 109 | export default Tooltip; 110 | 111 | -------------------------------------------------------------------------------- /src/components/ContextMenu/useContextMenu.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted, toRefs, shallowReactive, CSSProperties } from 'vue' 2 | import { IG6GraphEvent } from '@antv/g6' 3 | import { useContext } from '../../GraphinContext'; 4 | 5 | export interface ContextMenuProps { 6 | bindType?: 'node' | 'edge' | 'canvas'; 7 | bindEvent?: 'click' | 'contextmenu'; 8 | // container: React.RefObject; 9 | container?: any; // ref 10 | style?: CSSProperties; // ref 11 | } 12 | 13 | export interface ContextMenuState { 14 | /** 当前状态 */ 15 | visible: boolean; 16 | x: number; 17 | y: number; 18 | /** 触发的元素 */ 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | item?: IG6GraphEvent['item']; 21 | /** 只有绑定canvas的时候才触发 */ 22 | selectedItems: IG6GraphEvent['item'][]; 23 | onClose?: () => void; 24 | onShow?: (e: IG6GraphEvent) => void; 25 | } 26 | 27 | export const useContextMenu = (props: ContextMenuProps) => { 28 | const { bindType = 'node', bindEvent='contextmenu', container } = props; 29 | // @ts-ignore 30 | const { graph } = useContext(); 31 | 32 | const state = shallowReactive({ 33 | visible: false, 34 | x: 0, 35 | y: 0, 36 | selectedItems: [], 37 | } as ContextMenuState) 38 | 39 | const handleShow = state.onShow = (e: IG6GraphEvent) => { 40 | e.preventDefault(); 41 | e.stopPropagation(); 42 | 43 | const width: number = graph.get('width'); 44 | const height: number = graph.get('height'); 45 | if (!container.value) { 46 | return; 47 | } 48 | 49 | const bbox = container.value.getBoundingClientRect(); 50 | 51 | const offsetX = graph.get('offsetX') || 0; 52 | const offsetY = graph.get('offsetY') || 0; 53 | 54 | const graphTop = graph.getContainer().offsetTop; 55 | const graphLeft = graph.getContainer().offsetLeft; 56 | 57 | let x = e.canvasX + graphLeft + offsetX; 58 | let y = e.canvasY + graphTop + offsetY; 59 | 60 | // when the menu is (part of) out of the canvas 61 | 62 | if (x + bbox.width > width) { 63 | x = e.canvasX - bbox.width - offsetX + graphLeft; 64 | } 65 | if (y + bbox.height > height) { 66 | y = e.canvasY - bbox.height - offsetY + graphTop; 67 | } 68 | 69 | if (bindType === 'node') { 70 | // 如果是节点,则x,y指定到节点的中心点 71 | // eslint-disable-next-line no-underscore-dangle 72 | const { x: PointX, y: PointY } = (e.item && e.item.getModel()) as { x: number; y: number }; 73 | const CenterCanvas = graph.getCanvasByPoint(PointX, PointY); 74 | 75 | const daltX = e.canvasX - CenterCanvas.x; 76 | const daltY = e.canvasY - CenterCanvas.y; 77 | x = x - daltX; 78 | y = y - daltY; 79 | } 80 | 81 | /** 设置变量 */ 82 | state.visible = true 83 | state.x = x 84 | state.y = y 85 | state.item = e.item 86 | }; 87 | const handleClose = state.onClose = () => { 88 | state.visible = false 89 | state.x = 0 90 | state.y = 0 91 | }; 92 | 93 | const handleSaveAllItem = (e: IG6GraphEvent) => { 94 | state.selectedItems = e.selectedItems as IG6GraphEvent['item'][] 95 | } 96 | 97 | onMounted(() => { 98 | // @ts-ignore 99 | graph.on(`${bindType}:${bindEvent}`, handleShow); 100 | // 如果是左键菜单,可能导致和canvans的click冲突 101 | if (!(bindType == 'canvas' && bindEvent == 'click')) { 102 | graph.on('canvas:click', handleClose); 103 | } 104 | graph.on('canvas:drag', handleClose); 105 | graph.on('wheelzoom', handleClose); 106 | if (bindType === 'canvas') { 107 | graph.on('nodeselectchange', handleSaveAllItem); 108 | } 109 | }) 110 | onUnmounted(() => { 111 | // @ts-ignore 112 | graph.off(`${bindType}:${bindEvent}`, handleShow); 113 | graph.off('canvas:click', handleClose); 114 | graph.off('canvas:drag', handleClose); 115 | graph.off('wheelzoom', handleClose); 116 | graph.off('nodeselectchange', handleSaveAllItem); 117 | }) 118 | 119 | return state 120 | }; 121 | 122 | export default useContextMenu; 123 | -------------------------------------------------------------------------------- /src/components/Hull/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import G6 from '@antv/g6'; 3 | import { useContext, contextSymbol } from '../../GraphinContext'; 4 | import { defineComponent, onMounted, onUnmounted, ref, watch, DefineComponent } from 'vue'; 5 | import { debounce } from '@antv/util' 6 | 7 | const defaultHullCfg = { 8 | members: [], 9 | type: 'round-convex', 10 | nonMembers: [], 11 | style: { 12 | fill: 'lightblue', 13 | stroke: 'blue', 14 | opacity: 0.2, 15 | }, 16 | padding: 10, 17 | }; 18 | 19 | /** 20 | * deep merge hull config 21 | * @param defaultCfg 22 | * @param cfg 23 | */ 24 | const deepMergeCfg = (defaultCfg: typeof defaultHullCfg, cfg: HullCfg) => { 25 | const { style: DefaultCfg = {}, ...defaultOtherCfg } = defaultCfg; 26 | const { style = {}, ...others } = cfg; 27 | return { 28 | ...defaultOtherCfg, 29 | ...others, 30 | style: { 31 | ...DefaultCfg, 32 | ...style, 33 | }, 34 | }; 35 | }; 36 | 37 | export interface HullCfgStyle { 38 | /** 39 | * @description 填充颜色 40 | * @default 'lightblue' 41 | */ 42 | fill: string; 43 | /** 44 | * @description 描边颜色 45 | * @default 'blue' 46 | */ 47 | stroke: string; 48 | /** 49 | * 50 | * @description 透明度 51 | * @default 0.2 52 | */ 53 | opacity: number; 54 | } 55 | export interface HullCfg { 56 | /** 57 | * @description 在包裹内部的节点实例或节点 Id 数组 58 | * @default [] 59 | * 60 | */ 61 | members: string[]; 62 | /** 63 | * 包裹的 id 64 | */ 65 | id?: string; 66 | /** 67 | * @description 包裹的类型 68 | * round-convex: 生成圆角凸包轮廓, 69 | * smooth-convex: 生成平滑凸包轮廓 70 | * bubble: 产生一种可以避开 nonMembers 的平滑凹包轮廓(算法)。 71 | * @default round-convex 72 | */ 73 | type?: 'round-convex' | 'smooth-convex' | 'bubble'; 74 | /** 不在轮廓内部的节点数组,只在 bubble 类型的包裹中生效 */ 75 | nonMembers?: string[]; 76 | /** 轮廓的样式属性 */ 77 | style?: Partial; 78 | /** 79 | * @description 轮廓边缘和内部成员的间距 80 | * @default 10 81 | */ 82 | padding?: number; 83 | } 84 | 85 | export interface IHullProps { 86 | /** 87 | * @description 配置 88 | */ 89 | options: HullCfg[]; 90 | } 91 | 92 | export const Hull: DefineComponent = defineComponent({ 93 | name: 'Hull', 94 | props: { 95 | options: { 96 | type: Array, 97 | default: () => [] 98 | } 99 | }, 100 | inject: [contextSymbol], 101 | setup(props) { 102 | const { graph } = useContext(); 103 | const hullInstances = ref([]) 104 | 105 | const handleAfterUpdateItem = debounce(() => { 106 | hullInstances.value.forEach((item, index) => { 107 | // 这里有bug,Hull.updateData报错,实际上是因为数据更新后hull已经被destroy了,需要重新生成 108 | if (item.group.destroyed) { 109 | hullInstances.value[index] = graph.createHull( 110 | // @ts-ignore 111 | deepMergeCfg(defaultHullCfg, { 112 | id: `${Math.random()}`, // Utils.uuid(), 113 | ...props.options[index], 114 | }), 115 | ) 116 | } else { 117 | item.updateData(item.members); 118 | } 119 | }); 120 | }) 121 | 122 | const createHulls = () => { 123 | hullInstances.value = props.options.map(item => { 124 | return graph.createHull( 125 | // @ts-ignore 126 | deepMergeCfg(defaultHullCfg, { 127 | id: `${Math.random()}`, // Utils.uuid(), 128 | ...item, 129 | }), 130 | ); 131 | }) 132 | } 133 | watch(() => props.options, () => { 134 | // 如果options有更改,先删除再创建 135 | if (hullInstances.value && hullInstances.value.length) { 136 | hullInstances.value.forEach(item => graph.removeHull(item)); 137 | } 138 | createHulls() 139 | }) 140 | onMounted(() => { 141 | createHulls() 142 | graph.on('afterupdateitem', handleAfterUpdateItem); 143 | graph.on('aftergraphrefreshposition', handleAfterUpdateItem); 144 | }) 145 | onUnmounted(() => { 146 | graph.off('afterupdateitem', handleAfterUpdateItem); 147 | graph.off('aftergraphrefreshposition', handleAfterUpdateItem); 148 | }) 149 | 150 | return () => null 151 | } 152 | }) 153 | 154 | export default Hull; 155 | -------------------------------------------------------------------------------- /src/components/Tooltip/useTooltip.ts: -------------------------------------------------------------------------------- 1 | // @ts-no-check 2 | import { onMounted, onUnmounted, toRefs, shallowReactive, ref } from 'vue' 3 | import { IG6GraphEvent } from '@antv/g6' 4 | import { useContext } from '../../GraphinContext'; 5 | 6 | export interface Props { 7 | bindType?: 'node' | 'edge'; 8 | // container: React.RefObject; 9 | container: any; // ref 10 | } 11 | 12 | export interface State { 13 | /** 当前状态 */ 14 | visible: boolean; 15 | x: number; 16 | y: number; 17 | /** 触发的元素 */ 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | item: IG6GraphEvent['item']; 20 | } 21 | 22 | export const useTooltip = (props: Props) => { 23 | const { bindType = 'node', container } = props; 24 | // @ts-ignore 25 | const { graph } = useContext(); 26 | 27 | const state = shallowReactive({ 28 | visible: false, 29 | x: 0, 30 | y: 0, 31 | item: null, 32 | } as State) 33 | 34 | const timer = ref() 35 | 36 | const handleShow = (e: IG6GraphEvent) => { 37 | e.preventDefault(); 38 | e.stopPropagation(); 39 | 40 | if (timer.value) { 41 | clearTimeout(timer.value) 42 | } 43 | 44 | const point = graph.getPointByClient(e.clientX, e.clientY); 45 | let { x, y } = graph.getCanvasByPoint(point.x, point.y); 46 | if (bindType === 'node') { 47 | // 如果是节点,则x,y指定到节点的中心点 48 | // eslint-disable-next-line no-underscore-dangle 49 | if (e.item) { 50 | const { x: PointX = 0, y: PointY = 0 } = e.item.getModel(); 51 | const CenterCanvas = graph.getCanvasByPoint(PointX, PointY); 52 | 53 | const daltX = e.canvasX - CenterCanvas.x; 54 | const daltY = e.canvasY - CenterCanvas.y; 55 | x = x - daltX; 56 | y = y - daltY; 57 | } 58 | } 59 | 60 | /** 设置变量 */ 61 | state.visible = true 62 | state.item = e.item 63 | state.x = x 64 | state.y = y 65 | }; 66 | const handleClose = () => { 67 | if (timer.value) { 68 | clearTimeout(timer.value) 69 | } 70 | timer.value = setTimeout(() => { 71 | state.visible = false 72 | state.x = 0 73 | state.y = 0 74 | }, 200) 75 | }; 76 | 77 | const handleDragStart = () => { 78 | state.visible = false 79 | state.x = 0 80 | state.y = 0 81 | state.item = null 82 | }; 83 | 84 | const handleDragEnd = (e: IG6GraphEvent) => { 85 | const point = graph.getPointByClient(e.clientX, e.clientY); 86 | let { x, y } = graph.getCanvasByPoint(point.x, point.y); 87 | if (bindType === 'node') { 88 | // 如果是节点,则x,y指定到节点的中心点 89 | // eslint-disable-next-line no-underscore-dangle 90 | if (e.item) { 91 | const { x: PointX = 0, y: PointY = 0 } = e.item.getModel(); 92 | const CenterCanvas = graph.getCanvasByPoint(PointX, PointY); 93 | 94 | const daltX = e.canvasX - CenterCanvas.x; 95 | const daltY = e.canvasY - CenterCanvas.y; 96 | x = x - daltX; 97 | y = y - daltY; 98 | } 99 | 100 | state.visible = true 101 | state.x = x 102 | state.y = y 103 | } 104 | }; 105 | 106 | const removeTimer = () => { 107 | clearTimeout(timer.value); 108 | }; 109 | 110 | onMounted(() => { 111 | // @ts-ignore 112 | graph.on(`${bindType}:mouseenter`, handleShow); 113 | graph.on(`${bindType}:mouseleave`, handleClose); 114 | graph.on(`afterremoveitem`, handleClose); 115 | graph.on(`node:dragstart`, handleDragStart); 116 | graph.on(`node:dragend`, handleDragEnd); 117 | 118 | if (container.value) { 119 | container.value.addEventListener('mouseenter', removeTimer); 120 | container.value.addEventListener('mouseleave', handleClose); 121 | } 122 | }) 123 | onUnmounted(() => { 124 | // @ts-ignore 125 | console.log('effect..remove....'); 126 | graph.off(`${bindType}:mouseenter`, handleShow); 127 | graph.off(`${bindType}:mouseleave`, handleClose); 128 | graph.off(`afterremoveitem`, handleClose); 129 | graph.off(`node:dragstart`, handleDragStart); 130 | graph.off(`node:dragend`, handleDragEnd); 131 | if (container.value) { 132 | container.value.removeEventListener('mouseenter', removeTimer); 133 | container.value.removeEventListener('mouseleave', handleClose); 134 | } 135 | }) 136 | 137 | return state; 138 | }; 139 | 140 | export default useTooltip; 141 | -------------------------------------------------------------------------------- /example/App.vue: -------------------------------------------------------------------------------- 1 | 63 | 271 | 272 | 278 | -------------------------------------------------------------------------------- /src/Graphin.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { defineComponent, DefineComponent, onMounted, onUnmounted, ref, watch, toRaw, markRaw, shallowReactive, h } from 'vue'; 3 | 4 | import G6, { Graph as IGraph, GraphData, GraphOptions, TreeGraphData } from '@antv/g6'; 5 | // import React, { ErrorInfo } from 'react'; 6 | /** 内置API */ 7 | // import GraphinType from '@antv/graphin/es/Graphin'; 8 | import ApiController from '@antv/graphin/es/apis'; 9 | import { ApisType } from '@antv/graphin/es/apis/types'; 10 | /** 内置 Behaviors */ 11 | import * as Behaviors from './behaviors'; 12 | import { DEFAULT_TREE_LATOUT_OPTIONS, TREE_LAYOUTS } from '@antv/graphin/es/consts'; 13 | /** Context */ 14 | // import GraphinContext from './GraphinContext'; 15 | // import '@antv/graphin/es/index.css' 16 | /** 内置布局 */ 17 | import LayoutController from '@antv/graphin/es/layout'; 18 | import { getDefaultStyleByTheme, ThemeData } from '@antv/graphin/es/theme/index'; 19 | /** types */ 20 | import { GraphinData, GraphinProps, GraphinTreeData, IUserNode } from '@antv/graphin/es/typings/type'; 21 | // import cloneDeep from '@antv/graphin/es/utils/cloneDeep'; 22 | /** utils */ 23 | // import shallowEqual from './utils/shallowEqual'; 24 | // import deepEqual from '@antv/graphin/es/utils/deepEqual'; 25 | import { cloneDeep, isEqual as deepEqual } from 'lodash-es'; 26 | 27 | const { DragCanvas, ZoomCanvas, DragNode, DragCombo, ClickSelect, BrushSelect, ResizeCanvas } = Behaviors; 28 | import {createContext} from './GraphinContext' 29 | 30 | 31 | export const Graphin: DefineComponent = defineComponent({ 32 | 33 | name: "Graphin", 34 | 35 | props: { 36 | data: { 37 | type: Object, 38 | default: () => ({} as GraphinProps['data']) 39 | }, 40 | layout: { 41 | type: Object, 42 | default: () => ({} as GraphinProps['layout']) 43 | }, 44 | plugins: { 45 | type: Array, 46 | default: () => ([]) 47 | }, 48 | height: { 49 | type: Number, 50 | default: () => 600 51 | }, 52 | width: { 53 | type: Number, 54 | default: () => 800 55 | }, 56 | layoutCache: { 57 | type: Boolean, 58 | default: () => false 59 | }, 60 | modes: { 61 | type: Object, 62 | default: () => ({default: []}) 63 | }, 64 | style: { 65 | type: Object, 66 | default: () => ({}) 67 | }, 68 | containerId: { 69 | type: String, 70 | default: () => '' 71 | }, 72 | containerStyle: { 73 | type: Object, 74 | default: () => ({}) 75 | }, 76 | fitView: { 77 | type: Boolean, 78 | default: () => false 79 | }, 80 | fitViewPadding: { 81 | type: Number, 82 | default: () => 0 83 | }, 84 | fitCenter: { 85 | type: Boolean, 86 | default: () => false 87 | }, 88 | linkCenter: { 89 | type: Boolean, 90 | default: () => false 91 | }, 92 | groupByTypes: { 93 | type: Boolean, 94 | default: () => true 95 | }, 96 | autoPaint: { 97 | type: Boolean, 98 | default: () => true 99 | }, 100 | animate: { 101 | type: Boolean, 102 | default: () => false 103 | }, 104 | animateCfg: { 105 | type: Object, 106 | default: () => ({}) 107 | }, 108 | minZoom: { 109 | type: Number, 110 | default: () => 0.2 111 | }, 112 | maxZoom: { 113 | type: Number, 114 | default: () => 10 115 | }, 116 | enabledStack: { 117 | type: Boolean, 118 | default: () => false 119 | }, 120 | theme: { 121 | type: Object, 122 | default: () => ({}) 123 | }, 124 | }, 125 | 126 | components: { DragCanvas, ZoomCanvas, DragNode, DragCombo, ClickSelect, BrushSelect, ResizeCanvas }, 127 | 128 | setup(props, { slots }) { 129 | 130 | const { data, layout, width, height, layoutCache, plugins, ...otherOptions } = props; 131 | 132 | /** 传递给LayoutController的对象 */ 133 | const self = markRaw({ 134 | props, 135 | data, 136 | plugins, 137 | isTree: 138 | Boolean(props.data && (props.data as GraphinTreeData).children) || 139 | TREE_LAYOUTS.indexOf(String(layout && layout.type)) !== -1, 140 | graph: {} as IGraph, 141 | height: Number(height), 142 | width: Number(width), 143 | 144 | theme: {} as ThemeData, 145 | apis: {} as ApisType, 146 | layoutCache, 147 | layout: {} as LayoutController, 148 | dragNodes: [] as IUserNode[], 149 | 150 | options: { ...otherOptions } as GraphOptions, 151 | }) 152 | // self.props不会同步变化 153 | watch(() => props, (newProps) => self.props = {...newProps}, { deep: true }) 154 | 155 | /** Graph的DOM */ 156 | const graphDOM = ref(null); 157 | 158 | /** createContext内的数据 */ 159 | const contextRef = shallowReactive({ 160 | graph: {} as IGraph, 161 | apis: {} as ApisType, 162 | theme: {} as ThemeType, 163 | layout: {} as LayoutController, 164 | dragNodes: [], 165 | isReady: false, 166 | }); 167 | 168 | createContext(contextRef); 169 | 170 | const initData = (newData: GraphinProps['data']) => { 171 | if ((newData as GraphinTreeData).children) { 172 | self.isTree = true; 173 | } 174 | self.data = cloneDeep(newData); 175 | }; 176 | 177 | const initGraphInstance = () => { 178 | const { 179 | theme, 180 | data, 181 | layout, 182 | width, 183 | height, 184 | defaultCombo = { style: {}, type: 'graphin-combo' }, 185 | defaultEdge = { style: {}, type: 'graphin-line' }, 186 | defaultNode = { style: {}, type: 'graphin-circle' }, 187 | nodeStateStyles, 188 | edgeStateStyles, 189 | comboStateStyles, 190 | modes = { default: [] }, 191 | animate, 192 | handleAfterLayout, 193 | ...otherOptions 194 | } = props; 195 | 196 | if (modes.default.length > 0) { 197 | // TODO :给用户正确的引导,推荐使用Graphin的Behaviors组件 198 | console.info('%c suggestion: you can use @antv/graphin Behaviors components', 'color:lightgreen'); 199 | } 200 | 201 | /** width and height */ 202 | const { clientWidth, clientHeight } = graphDOM.value as HTMLDivElement; 203 | /** shallow clone */ 204 | initData(props.data); 205 | 206 | /** 重新计算宽度 */ 207 | self.width = Number(width) || clientWidth || 500; 208 | self.height = Number(height) || clientHeight || 500; 209 | 210 | const themeResult = getDefaultStyleByTheme(props.theme); 211 | 212 | const { 213 | defaultNodeStyle = {}, 214 | defaultEdgeStyle = {}, 215 | defaultComboStyle = {}, 216 | defaultNodeStatusStyle = {}, 217 | defaultEdgeStatusStyle = {}, 218 | defaultComboStatusStyle = {}, 219 | ...otherTheme 220 | } = themeResult; 221 | 222 | /** graph type */ 223 | self.isTree = 224 | Boolean((data as GraphinTreeData).children) || TREE_LAYOUTS.indexOf(String(props.layout && props.layout.type)) !== -1; 225 | 226 | const finalStyle = markRaw({ 227 | defaultNode: { style: { ...defaultNode.style, defaultNodeStyle, _theme: theme }, type: defaultNode.type || 'graphin-circle' }, // isGraphinNodeType ? deepMix({}, defaultNodeStyle, defaultNode) : defaultNode, 228 | defaultEdge: { style: { ...defaultEdge.style, defaultEdgeStyle, _theme: theme }, type: defaultEdge.type || 'graphin-line' }, // isGraphinEdgeType ? deepMix({}, defaultEdgeStyle, defaultEdge) : defaultEdge, 229 | defaultCombo: { style: { ...defaultCombo.style, defaultComboStyle, _theme: theme }, type: defaultCombo.type || 'combo' }, // deepMix({}, defaultComboStyle, defaultCombo), // TODO:COMBO的样式需要内部自定义 230 | /** status 样式 */ 231 | nodeStateStyles: { ...nodeStateStyles, defaultNodeStatusStyle }, // isGraphinNodeType ? deepMix({}, defaultNodeStatusStyle, nodeStateStyles) : nodeStateSty les, 232 | edgeStateStyles: { ...edgeStateStyles, defaultEdgeStatusStyle }, // isGraphinEdgeType ? deepMix({}, defaultEdgeStatusStyle, edgeStateStyles) : edgeStateSty les, 233 | comboStateStyles: { ...comboStateStyles, defaultComboStatusStyle }, // deepMix({}, defaultComboStatusStyle, comboStateStyles), 234 | }); 235 | 236 | contextRef.theme = self.theme = { ...finalStyle, ...otherTheme } as unknown as ThemeData; 237 | self.options = markRaw({ 238 | container: graphDOM.value, 239 | renderer: 'canvas', 240 | width: self.width, 241 | height: self.height, 242 | animate: animate !== false, 243 | ...finalStyle, 244 | modes, 245 | ...otherOptions, 246 | }) as GraphOptions; 247 | 248 | if (self.isTree) { 249 | self.options.layout = layout || DEFAULT_TREE_LATOUT_OPTIONS, 250 | self.graph = new G6.TreeGraph(self.options as GraphOptions); 251 | } else { 252 | self.graph = new G6.Graph(self.options as GraphOptions); 253 | } 254 | contextRef.graph = self.graph 255 | 256 | /** 内置事件:AfterLayout 回调 */ 257 | self.graph.on('afterlayout', () => { 258 | if (handleAfterLayout) { 259 | handleAfterLayout(self.graph as IGraph); 260 | } 261 | }); 262 | 263 | /** 装载数据 */ 264 | self.graph.data(self.data as GraphData | TreeGraphData); 265 | 266 | /** 初始化布局:仅限网图 */ 267 | if (!self.isTree) { 268 | // 这里需要将self当作graphin的对象传到LayoutController里面,所以先将graphDOM赋值以下 269 | self.graphDOM = graphDOM.value 270 | self.context = contextRef 271 | self.props = markRaw({...props}) 272 | contextRef.layout = self.layout = new LayoutController(self); 273 | self.layout.start(); 274 | } 275 | 276 | /** 渲染 */ 277 | self.graph.render(); 278 | /** FitView 变为组件可选 */ 279 | 280 | /** 初始化状态 */ 281 | initStatus(); 282 | /** 生成API */ 283 | contextRef.apis = ApiController(self.graph as IGraph); 284 | 285 | contextRef.isReady = true; 286 | }; 287 | 288 | /** 初始化状态 */ 289 | const initStatus = () => { 290 | if (!self.isTree) { 291 | const { nodes = [], edges = [] } = props.data as GraphinData; 292 | nodes.forEach((node) => { 293 | const { status } = node; 294 | if (status) { 295 | Object.keys(status).forEach((k) => { 296 | self.graph.setItemState(node.id, k, Boolean(status[k])); 297 | }); 298 | } 299 | }); 300 | edges.forEach((edge) => { 301 | const { status } = edge; 302 | if (status) { 303 | Object.keys(status).forEach((k) => { 304 | self.graph.setItemState(edge.id, k, Boolean(status[k])); 305 | }); 306 | } 307 | }); 308 | } 309 | }; 310 | 311 | onMounted(() => { 312 | initGraphInstance(); 313 | }); 314 | 315 | // dataChange 316 | watch( 317 | () => props.data, 318 | (v) => { 319 | let newDragNodes: IUserNode[]; 320 | 321 | /** 数据变化 */ 322 | if (!deepEqual(toRaw(v), toRaw(self.data))) { 323 | initData(v); 324 | 325 | if (self.isTree) { 326 | self.graph.changeData(self.data as TreeGraphData); 327 | } else { 328 | // 若 dragNodes 中的节点已经不存在,则从数组中删去 329 | // @ts-ignore 330 | newDragNodes = self.dragNodes.filter( 331 | dNode => (self.data as GraphinData).nodes && (self.data as GraphinData).nodes.find(node => node.id === dNode.id), 332 | ); 333 | // 更新拖拽后的节点的mass到data 334 | // @ts-ignore 335 | if (self.data.nodes && self.data.nodes.length > 0) { 336 | self.data.nodes.forEach(node => { 337 | const dragNode = newDragNodes.find(item => item.id === node.id); 338 | if (dragNode) { 339 | const { force={} } = dragNode.layout || {} 340 | node.layout = { 341 | ...node.layout, 342 | force: { 343 | mass: force.mass, 344 | }, 345 | }; 346 | } 347 | }) 348 | } 349 | } 350 | 351 | self.graph.data(self.data as GraphData | TreeGraphData); 352 | self.graph.set('layoutController', null); 353 | self.graph.changeData(self.data as GraphData | TreeGraphData); 354 | 355 | // 由于 changeData 是将 this.data 融合到 item models 上面,因此 changeData 后 models 与 this.data 不是同一个引用了 356 | // 执行下面一行以保证 graph item model 中的数据与 this.data 是同一份 357 | // @ts-ignore 358 | self.data = self.layout.getDataFromGraph(); 359 | self.layout.changeLayout(); 360 | 361 | initStatus(); 362 | contextRef.apis = ApiController(self.graph as IGraph); 363 | contextRef.dragNodes = self.dragNodes = newDragNodes || self.dragNodes 364 | self.graph.emit('graphin:datachange'); 365 | } 366 | }, 367 | ); 368 | // layout 更新 369 | watch( 370 | () => props.layout, 371 | (layout, prevLayout) => { 372 | if (self.isTree) { 373 | self.graph.updateLayout(layout); 374 | return 375 | } 376 | /** 377 | * TODO 378 | * 1. preset 前置布局判断问题 379 | * 2. enablework 问题 380 | * 3. G6 LayoutController 里的逻辑 381 | */ 382 | /** 数据需要从画布中来 */ 383 | // @ts-ignore 384 | self.data = self.layout.getDataFromGraph() 385 | self.layout.changeLayout(); 386 | self.graph.emit('graphin:layoutchange', { prevLayout: prevLayout, layout }); 387 | }, 388 | ); 389 | 390 | watch( 391 | () => props.layoutCache, 392 | (v) => { 393 | self.layoutCache = v; 394 | }, 395 | ); 396 | 397 | const clear = () => { 398 | if (self.layout && self.layout.destroy) { 399 | self.layout.destroy(); // tree graph 400 | } 401 | self.layout = {} as LayoutController; 402 | self.graph!.clear(); 403 | self.data = { nodes: [], edges: [], combos: [] }; 404 | self.graph!.destroy(); 405 | }; 406 | 407 | onUnmounted(() => { 408 | clear(); 409 | }); 410 | 411 | return () => { 412 | const { containerId, style, modes, containerStyle, } = props 413 | const { isReady, theme={} } = contextRef 414 | const { background } = theme 415 | return h('div', { 416 | id: containerId || "graphin-container", 417 | style: { 418 | height: '100%', 419 | width: '100%', 420 | position: 'relative', 421 | ...containerStyle, 422 | } 423 | }, h('div', { 424 | 'data-testid': 'custom-element', 425 | 'class': 'graphin-core', 426 | ref: graphDOM, 427 | style: { 428 | height: '100%', 429 | width: '100%', 430 | minHeight: '500px', 431 | background: background ? background : undefined, 432 | ...style 433 | } 434 | }), h('div', {'class': 'graphin-components'}, isReady ? h('div', {}, [ 435 | /** @ts-ignore modes 不存在的时候,才启动默认的behaviors,否则会覆盖用户自己传入的 */ 436 | modes.default.length == 0 ? h('div', {}, [ 437 | /* 拖拽画布 */ 438 | h(DragCanvas), 439 | /* 缩放画布 */ 440 | h(ZoomCanvas), 441 | /* 拖拽节点 */ 442 | h(DragNode), 443 | /* 拖拽Combo */ 444 | h(DragCombo), 445 | /* 点击节点 */ 446 | h(ClickSelect), 447 | /* 圈选节点 */ 448 | h(BrushSelect), 449 | ]) : null, 450 | slots.default ? slots.default() : null, 451 | graphDOM.value ? h(ResizeCanvas, {graphDOM: graphDOM.value as HTMLDivElement}) : null, 452 | ]) : null)) 453 | } 454 | }, 455 | }) 456 | 457 | export default Graphin 458 | --------------------------------------------------------------------------------