├── 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 |
13 |
14 |
{{scope.model.id}}
15 | {{scope.model.id}}
16 | {{scope.model.id}}
17 | {{scope.model.id}}
18 | {{scope.model.id}}
19 |
20 |
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 |
24 |
28 |
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 |
4 |
5 |
6 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
{{scope.model.id}}
38 | {{scope.model.id}}
39 | {{scope.model.id}}
40 | {{scope.model.id}}
41 | {{scope.model.id}}
42 | {{scope.model.id}}
43 | {{scope.model.id}}
44 |
45 |
46 |
47 |
59 |
60 |
61 |
62 |
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 |
--------------------------------------------------------------------------------