angular使用antVx6,实现okr树形以及脑图

一、上手

创建angular项目的流程我这边就不详细写了,我用的angular版本是11,这个版本比较新,当然整合antv/x6也需要做点工作。

第一步:下载依赖包

1
2
3
yarn add @antv/x6
yarn add  @types/jquery -D
yarn add  @types/jquery-mousewheel -D

第二步:配置angular.json文件

1
2
3
4
5
6
7
8
"architect":{
       "build":{
               "allowedCommonJsDependencies": [
                       "jquery",
                        "mousetrap"
       ]
       }
}

ps:当你安装好x6之后,然后import 模块再配置angular.json文件最后跑的时候你会发现报错了,这个时候我们直接根据项目报错提示安装相应缺失的依赖包即可,具体要安装@types/jquery、@types/jquery-mousewheel两个依赖包。如果angular版本很低可能还会需要安装@types/lodash-es、@types/mousetrap这两个依赖包。

二、树形okr具体实现

详细代码参照官网实例:https://x6.antv.vision/zh/examples/showcase/practices#orgchart

转化为ts格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
import {Component, ViewChild} from '@angular/core';
import { Graph, Node, Point , Cell,  Color, Dom  } from '@antv/x6'
import dagre from 'dagre'

Graph.registerNode(
  'org-node',
  {
    width: 260,
    height: 88,
    markup: [
      {
        tagName: 'rect',
        attrs: {
          class: 'card',
        },
      },
      {
        tagName: 'rect',
        attrs: {
          class: 'labelcolor',
        },
      },
      {
        tagName: 'image',
        attrs: {
          class: 'image',
        },
      },
      {
        tagName: 'text',
        attrs: {
          class: 'rank',
        },
      },
      {
        tagName: 'text',
        attrs: {
          class: 'name',
        },
      },
      {
        tagName: 'g',
        attrs: {
          class: 'btn add',
        },
        children: [
          {
            tagName: 'circle',
            attrs: {
              class: 'add',
            },
          },
          {
            tagName: 'text',
            attrs: {
              class: 'add',
            },
          },
        ],
      },
      {
        tagName: 'g',
        attrs: {
          class: 'btn del',
        },
        children: [
          {
            tagName: 'circle',
            attrs: {
              class: 'del',
            },
          },
          {
            tagName: 'text',
            attrs: {
              class: 'del',
            },
          },
        ],
      },
    ],
    attrs: {
      '.card': {
        rx: 10,
        ry: 10,
        refWidth: '100%',
        refHeight: '100%',
        fill: '#FFF',
        stroke: '#000',
        strokeWidth: 0,
        pointerEvents: 'visiblePainted',
      },
      '.labelcolor':{
        x: 0,
        y:0,
        width:5,
        refHeight: '100%',
        fill: '#000',
      },
      '.image': {
        x: 16,
        y: 16,
        width: 56,
        height: 56,
        opacity: 0.7,
      },
      '.rank': {
        refX: 0.95,
        refY: 0.5,
        fontFamily: 'Courier New',
        fontSize: 13,
        textAnchor: 'end',
        textVerticalAnchor: 'middle',
      },
      '.name': {
        refX: 0.95,
        refY: 0.7,
        fontFamily: 'Arial',
        fontSize: 14,
        fontWeight: '600',
        textAnchor: 'end',
      },
      '.btn.add': {
        refDx: -16,
        refY: 16,
        event: 'node:add',
      },
      '.btn.del': {
        refDx: -44,
        refY: 16,
        event: 'node:delete',
      },
      '.btn > circle': {
        r: 10,
        fill: 'transparent',
        stroke: '#333',
        strokeWidth: 1,
      },
      '.btn.add > text': {
        fontSize: 20,
        fontWeight: 800,
        stroke: '#000',
        x: -5.5,
        y: 7,
        fontFamily: 'Times New Roman',
        text: '+',
      },
      '.btn.del > text': {
        fontSize: 28,
        fontWeight: 500,
        stroke: '#000',
        x: -4.5,
        y: 6,
        fontFamily: 'Times New Roman',
        text: '-',
      },
    },
  },
  true,
)

// 自定义边
Graph.registerEdge(
  'org-edge',
  {
    zIndex: -1,
    attrs: {
      line: {
        stroke: '#585858',
        strokeWidth: 3,
        sourceMarker: null,
        targetMarker: null,
      },
    },
  },
  true,
)

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less']
})
export class AppComponent {
  title = 'angular-okr-component';
  width=document.body.clientWidth;

  ngAfterViewInit(){
    this.init();
  }
  init(){
    let height=document.body.clientHeight+15;
    this.graph = new Graph({
      container: document.getElementById('container'),
      grid: true,
      panning: {
        enabled: false,
      },
      width: this.width,
      height: height,
      scroller: true,
      snapline: true,
      interacting: false,
    })
    let nodes = [
      this.createNode('Founder & Chairman', 'Pierre Omidyar', this.male, '#ddd'),
      this.createNode('President & CEO', 'Margaret C. Whitman', this.female, '#ddd'),
      this.createNode('President, PayPal', 'Scott Thompson', this.male, '#ddd'),
      this.createNode('President, Ebay Global Marketplaces','Devin Wenig',this.male,'#ddd',),
      this.createNode('Senior Vice President Human Resources','Jeffrey S. Skoll',this.male,'#ddd',),
      this.createNode('Senior Vice President Controller','Steven P. Westly',this.male,'#ddd',),
    ]
     let edges = [
      this.createEdge(nodes[0], nodes[1]),
      this.createEdge(nodes[1], nodes[2]),
      this.createEdge(nodes[1], nodes[3]),
      this.createEdge(nodes[1], nodes[4]),
      this.createEdge(nodes[1], nodes[5]),
    ]
    this.graph.resetCells([...nodes, ...edges])
    this.layout()
    this.graph.zoomTo(0.8)
    this.graph.centerContent()
    this.setup()
  }
  constructor() {}
   male =
    'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*kUy8SrEDp6YAAAAAAAAAAAAAARQnAQ'
   female =
    'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*f6hhT75YjkIAAAAAAAAAAAAAARQnAQ'
  // 布局方向
   dir = 'TB' // LR RL TB BT
  // 创建画布
   graph;
  // 监听自定义事件
   setup() {
     console.log("进入自定义事件")
    this.graph.on('node:add', ({ e, node }) => {
      console.log("进入增加事件")
    })
    this.graph.on('node:delete', ({ e, node }) => {
      console.log("进入减少事件")
    })
     this.graph.on('node:click', ({ e, x, y, node, view }) => {
       console.log("进入node点击事件")
     })
     this.graph.on('cell:contextmenu', ({ e, x, y, node, view }) => {
      console.log("进入右鍵点击事件")
      })
  }
  // 自动布局
 layout() {
    const nodes = this.graph.getNodes()
    const edges = this.graph.getEdges()
    const g = new dagre.graphlib.Graph()
    g.setGraph({ rankdir: this.dir, nodesep: 16, ranksep: 16 })
    g.setDefaultEdgeLabel(() => ({}))
    const width = 260
    const height = 90
    nodes.forEach((node) => {
      g.setNode(node.id, { width, height })
    })

    edges.forEach((edge) => {
      const source:any = edge.getSource()
      const target:any = edge.getTarget()
      g.setEdge(source.cell, target.cell)
    })
    dagre.layout(g)
    this.graph.freeze()

    g.nodes().forEach((id) => {
      const node = this.graph.getCell(id) as Node
      if (node) {
        const pos = g.node(id)
        node.position(pos.x, pos.y)
      }
    })

    edges.forEach((edge) => {
      const source = edge.getSourceNode()!
      const target = edge.getTargetNode()!
      const sourceBBox = source.getBBox()
      const targetBBox = target.getBBox()

      console.log(sourceBBox, targetBBox)

      if ((this.dir === 'LR' || this.dir === 'RL') && sourceBBox.y !== targetBBox.y) {
        const gap =
        this.dir === 'LR'
            ? targetBBox.x - sourceBBox.x - sourceBBox.width
            : -sourceBBox.x + targetBBox.x + targetBBox.width
        const fix = this.dir === 'LR' ? sourceBBox.width : 0
        const x = sourceBBox.x + fix + gap / 2
        edge.setVertices([
          { x, y: sourceBBox.center.y },
          { x, y: targetBBox.center.y },
        ])
      } else if (
        (this.dir === 'TB' || this.dir === 'BT') &&
        sourceBBox.x !== targetBBox.x
      ) {
        const gap =
        this.dir === 'TB'
            ? targetBBox.y - sourceBBox.y - sourceBBox.height
            : -sourceBBox.y + targetBBox.y + targetBBox.height
        const fix = this.dir === 'TB' ? sourceBBox.height : 0
        const y = sourceBBox.y + fix + gap / 2
        edge.setVertices([
          { x: sourceBBox.center.x, y },
          { x: targetBBox.center.x, y },
        ])
      } else {
        edge.setVertices([])
      }
    })
    this.graph.unfreeze()
  }
   createNode(
    rank: string,
    name: string,
    image: string,
    background: string,
    textColor = '#000',
  ) {
    return this.graph.createNode({
      shape: 'org-node',
      attrs: {
        '.card': { fill: '#ddd' },
        '.labelcolor':{fill: 'red'},
        '.image': { xlinkHref: image },
        '.rank': {
          fill: textColor,
          text: Dom.breakText(rank, { width: 160, height: 45 }),
        },
        '.name': {
          fill: textColor,
          text: Dom.breakText(name, { width: 160, height: 45 }),
        },
        '.btn > circle': { stroke: textColor },
        '.btn > text': { fill: textColor, stroke: textColor },
      },
    })
  }
   createEdge(source: Cell, target: Cell) {
    return this.graph.createEdge({
      shape: 'org-edge',
      source: { cell: source.id },
      target: { cell: target.id },
    })
  }
}

ps:里面应用了dagre框架,可以自动布局,省去很多时间。有兴趣的同学可以去看看https://github.com/dagrejs/dagre。实现效果如下,方向通过dir变量控制的,上下左右都可以,自行配置。
效果图

三、脑图实现

详细代码参照:https://x6.antv.vision/zh/examples/layout/tree#mindmap

转化为ts格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
import { Component } from '@angular/core';
import { Graph, Node, Point , Cell,  Color, Dom, Model } from '@antv/x6'
import Hierarchy from '@antv/hierarchy'
import {HierarchyResult} from './type'

Graph.registerNode(
  'rect',
  {
    width: 260,
    height: 88,
    markup: [
      {
        tagName: 'g',
        selector: 'buttonGroup',
        children: [
          {
            tagName: 'rect',
            selector: 'button',
            attrs: {
              'pointer-events': 'visiblePainted',
            },
          },
          {
            tagName: 'path',
            selector: 'buttonSign',
            attrs: {
              fill: 'none',
              // 'pointer-events': 'none',
              // d: 'M 1 5 9 5 M 5 1 5 9',
              strokeWidth: 1.6,
            },
          },
        ],
      },
      {
        tagName: 'text',
        attrs: {
          class: 'children',
        },
      },
      {
        tagName: 'rect',
        attrs: {
          class: 'card',
        },
      },
      {
        tagName: 'rect',
        attrs: {
          class: 'labelcolor',
        },
      },
      {
        tagName: 'image',
        attrs: {
          class: 'image',
        },
      },
      {
        tagName: 'text',
        attrs: {
          class: 'rank',
        },
      },
      {
        tagName: 'text',
        attrs: {
          class: 'name',
        },
      },
      {
        tagName: 'rect',
        attrs: {
          class: 'badgeBg',
        },
      },
      {
        tagName: 'text',
        attrs: {
          class: 'badge',
        },
      },
      {
        tagName: 'text',
        attrs: {
          class: 'content',
        },
      },
    ],
    attrs: {
      buttonGroup: {
        refX: '100%',
        refY: '50%',
      },
      button: {
        fill: '#4C65DD',
        stroke: 'none',
        position:'absolute',
        x: -10,
        y: -10,
        height: 20,
        width: 30,
        rx: 10,
        ry: 10,
        cursor: 'pointer',
        event: 'node:collapse',
      },
      buttonSign: {
        refX: 5,
        refY: -5,
        stroke: '#FFFFFF',
        strokeWidth: 1.6,
      },
      '.children':{
        fontSize: 10,
        stroke: '#FFFFFF',
        position:'absolute',
        x: 202,
        y: 47,
        height: 20,
        width: 30,
        event: 'node:collapse',
      },
      '.card': {
        rx: 10,
        ry: 10,
        refWidth: '100%',
        refHeight: '100%',
        fill: '#FFF',
        stroke: '#000',
        strokeWidth: 0,
        zIndex:9,
        pointerEvents: 'visiblePainted',
      },
      '.labelcolor':{
        x: 0,
        y:0,
        width:2,
        refHeight: '100%',
      },
      '.image': {
        x: 16,
        y: 16,
        width: 26,
        height: 26,
        // opacity: 0.7,
      },
      '.rank': {
        refX: 0.95,
        refY: 0.35,
        fontFamily: 'Courier New',
        fontSize: 13,
        textAnchor: 'end',
        textVerticalAnchor: 'middle',
      },
      '.name': {
        x: 85,
        y: 28,
        // refX: 0.95,
        // refY: 0.35,
        fontFamily: 'Arial',
        fontSize: 14,
        fontWeight: '600',
        textAnchor: 'end',
      },
      '.badgeBg':{
        x: 12,
        y: 53,
        width: 22,
        height: 17,
        fill: '#0197F8',
        rx: 7,
        ry: 7,
      },
     '.badge':{
        x: 18,
        y: 66,
        width: 26,
        height: 16,
        fontSize: 10,
        fontWeight: '100',
      },
      '.content':{
        x: 46,
        y: 66,
        width: 126,
        height: 26,
        fontSize: 12,
      },
    },
  },
  true,
)

// 自定义边
Graph.registerEdge(
  'org-edge',
  {
    zIndex: -1,
    attrs: {
      line: {
        stroke: '#585858',
        strokeWidth: 1,
        strokeLinejoin: 'round',
      },
    },
  },
  true,
)
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less']
})
export class AppComponent {

  ngAfterViewInit(){
    this.init();
  }

  graph;
  width=document.body.clientWidth;
  male =
    'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*kUy8SrEDp6YAAAAAAAAAAAAAARQnAQ'
  init(){
    let height=document.body.clientHeight+15;
    this.graph = new Graph({
      container: document.getElementById('container')!,
      background: {
        color: '#f5f5f5',
      },
      grid: {
        visible: false,
      },
      panning: {
        enabled: true,
      },
      width: this.width,
      height: height,
      scroller: true,
      // snapline: true,
      interacting: false,
      connecting: {
        anchor: 'orth',
        connector: 'rounded',
        connectionPoint: 'boundary',
        router: {
          name: 'er',
          args: {
            offset: 50,
            direction: 'H',
          },
        },
      },
    })

      const result = Hierarchy.mindmap(this.mindData, {
        direction: 'H',
        getHeight() {
          return 100
        },
        getWidth() {
          return 220
        },
        getHGap() {
          return 20
        },
        getVGap() {
          return 10
        },
        getSide: (d) => {
          console.log("222111111",d)
          // let direction=JSON.parse(JSON.stringify(d))
          // return 'right'
          if(d.children.length>5){
            return 'right'
          }else{
            return 'left'
          }
         
        },
      })
      const model: Model.FromJSONData = { nodes: [], edges: [] }
      const traverse = (data: HierarchyResult) => {
        if (data) {
          model.nodes?.push({
            id: `${data.id}`,
            x: data.x + 450,
            y: data.y + 450,
            shape: 'rect',
            width: 200,
            height: 90,
            attrs: {
              '.image': { xlinkHref: this.male },
              '.card': { fill: "#fff" },
              '.rank': {
                fill: 'red',
                text: Dom.breakText('fengfeng', { width: 60, height: 45 }),
              },
              '.labelcolor':{fill: '#0197F8'},
              '.name': {
                fill: '#000' ,
                text:'pppp',
              },
              '.badgeBg':{
                fill: '#0197F8',
              },
              '.badge':{
                stroke:'#fff',
                text:'32',
              },
              '.content':{
                fill: '#000' ,
                text:'这是内容这是内容',
              },
              '.children':{
                stroke:'#fff',
                text:'32',
              }
            },
          })
        }
        if (data.children) {
          data.children.forEach((item: HierarchyResult) => {
            model.edges?.push({
              shape: 'org-edge',
              source: `${data.id}`,
              target: `${item.id}`,
              attrs: {
                line: {
                  stroke: '#ccc',
                  strokeWidth: 1,
                  targetMarker: null,
                },
              },
            })
            traverse(item)
          })
        }
      }
      traverse(result)
      this.graph.fromJSON(model)
      this.graph.zoomTo(0.4)
      this.collapse()
  }
 
  toggleCollapse(collapsed?: boolean,node?: any,edge?:any) {
    const target = collapsed == null ? !this.collapsed : collapsed
    this.collapsed = target
  }
  collapsed: boolean = false
  collapse(){
    this.graph.on('node:collapse', ({ cell,node  }: {cell:any, node: any }) => {
      this.toggleCollapse(!this.collapsed,node)
      const run = (pre: any) => {
        const succ = this.graph.getSuccessors(pre, { distance: 1 })
        if (succ) {
          succ.forEach((node: any) => {
            node.toggleVisible(!this.collapsed)
            run(node)
          })
        }
      }
      run(node)
    })
  }
}

ps:其中Hierarchy框架是一个可视化分层数据的布局算法。有兴趣的同学可以学习https://github.com/antvis/hierarchy。getSide方法是改变脑图左右方向的。以上代码中mindData数据参考脑图官网数据即可。效果图如下:
效果图

如果想获取nodes节点数据,可以格式化nodes,执行命令JSON.parse(JSON.stringify(model.nodes)),既可操作里面的数据。例如要想根据每个card的children是否大于0来控制是否可以展示可以执行命令opacity:JSON.parse(JSON.stringify(model.nodes)).children.length>0?1:0,这样就可根据数据信息动态展示你所需要的内容以及样式。效果图如下:

效果图