@antv/layout 0.3.15 → 0.3.17-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,11 +3,25 @@
3
3
  * @author shiwu.wyy@antfin.com
4
4
  */
5
5
 
6
- import { Edge, OutNode, DagreLayoutOptions, PointTuple, Point, Node } from "./types";
6
+ import {
7
+ Edge,
8
+ OutNode,
9
+ DagreLayoutOptions,
10
+ PointTuple,
11
+ Point,
12
+ Node,
13
+ } from "./types";
7
14
  import dagre from "./dagre/index";
8
- import { isArray, isNumber, isObject, getEdgeTerminal, getFunc, isString } from "../util";
15
+ import {
16
+ isArray,
17
+ isNumber,
18
+ isObject,
19
+ getEdgeTerminal,
20
+ getFunc,
21
+ isString,
22
+ } from "../util";
9
23
  import { Base } from "./base";
10
- import { Graph as DagreGraph } from './dagre/graph';
24
+ import { Graph as DagreGraph } from "./dagre/graph";
11
25
 
12
26
  /**
13
27
  * 层次布局
@@ -57,8 +71,8 @@ export class DagreLayout extends Base {
57
71
 
58
72
  /** 上次的布局结果 */
59
73
  public preset: {
60
- nodes: OutNode[],
61
- edges: any[],
74
+ nodes: OutNode[];
75
+ edges: any[];
62
76
  };
63
77
 
64
78
  public nodes: OutNode[] = [];
@@ -70,7 +84,7 @@ export class DagreLayout extends Base {
70
84
 
71
85
  private nodeMap: {
72
86
  [id: string]: OutNode;
73
- }
87
+ };
74
88
 
75
89
  constructor(options?: DagreLayoutOptions) {
76
90
  super();
@@ -101,14 +115,23 @@ export class DagreLayout extends Base {
101
115
  return layout;
102
116
  }
103
117
  return true;
104
- }
118
+ };
105
119
 
106
120
  /**
107
121
  * 执行布局
108
122
  */
109
123
  public execute() {
110
124
  const self = this;
111
- const { nodes, nodeSize, rankdir, combos, begin, radial, comboEdges = [], vedges = [] } = self;
125
+ const {
126
+ nodes,
127
+ nodeSize,
128
+ rankdir,
129
+ combos,
130
+ begin,
131
+ radial,
132
+ comboEdges = [],
133
+ vedges = [],
134
+ } = self;
112
135
  if (!nodes) return;
113
136
  const edges = (self.edges as any[]) || [];
114
137
  const g = new DagreGraph({
@@ -119,12 +142,12 @@ export class DagreLayout extends Base {
119
142
  // collect the nodes in their combo, to create virtual edges for comboEdges
120
143
  self.nodeMap = {};
121
144
  const nodeComboMap = {} as any;
122
- nodes.forEach(node => {
145
+ nodes.forEach((node) => {
123
146
  self.nodeMap[node.id] = node;
124
147
  if (!node.comboId) return;
125
148
  nodeComboMap[node.comboId] = nodeComboMap[node.comboId] || [];
126
149
  nodeComboMap[node.comboId].push(node.id);
127
- })
150
+ });
128
151
 
129
152
  let nodeSizeFunc: (d?: any) => number[];
130
153
  if (!nodeSize) {
@@ -132,7 +155,8 @@ export class DagreLayout extends Base {
132
155
  if (d.size) {
133
156
  if (isArray(d.size)) {
134
157
  return d.size;
135
- } if (isObject(d.size)) {
158
+ }
159
+ if (isObject(d.size)) {
136
160
  return [d.size.width || 40, d.size.height || 40];
137
161
  }
138
162
  return [d.size, d.size];
@@ -178,35 +202,35 @@ export class DagreLayout extends Base {
178
202
  });
179
203
  }
180
204
 
181
- nodes.filter((node) => node.layout !== false).forEach((node) => {
182
- const size = nodeSizeFunc(node);
183
- const verti = vertisep(node);
184
- const hori = horisep(node);
185
- const width = size[0] + 2 * hori;
186
- const height = size[1] + 2 * verti;
187
- const layer = node.layer;
188
- if (isNumber(layer)) {
189
- // 如果有layer属性,加入到node的label中
190
- g.setNode(node.id, { width, height, layer });
191
- } else {
192
- g.setNode(node.id, { width, height });
193
- }
194
-
195
- if (this.sortByCombo && node.comboId) {
196
- if (!comboMap[node.comboId]) {
197
- comboMap[node.comboId] = { id: node.comboId };
198
- g.setNode(node.comboId, {});
205
+ nodes
206
+ .filter((node) => node.layout !== false)
207
+ .forEach((node) => {
208
+ const size = nodeSizeFunc(node);
209
+ const verti = vertisep(node);
210
+ const hori = horisep(node);
211
+ const width = size[0] + 2 * hori;
212
+ const height = size[1] + 2 * verti;
213
+ const layer = node.layer;
214
+ if (isNumber(layer)) {
215
+ // 如果有layer属性,加入到node的label中
216
+ g.setNode(node.id, { width, height, layer });
217
+ } else {
218
+ g.setNode(node.id, { width, height });
199
219
  }
200
- g.setParent(node.id, node.comboId);
201
- }
202
- });
203
-
204
220
 
221
+ if (this.sortByCombo && node.comboId) {
222
+ if (!comboMap[node.comboId]) {
223
+ comboMap[node.comboId] = { id: node.comboId };
224
+ g.setNode(node.comboId, {});
225
+ }
226
+ g.setParent(node.id, node.comboId);
227
+ }
228
+ });
205
229
 
206
230
  edges.forEach((edge) => {
207
231
  // dagrejs Wiki https://github.com/dagrejs/dagre/wiki#configuring-the-layout
208
- const source = getEdgeTerminal(edge, 'source');
209
- const target = getEdgeTerminal(edge, 'target');
232
+ const source = getEdgeTerminal(edge, "source");
233
+ const target = getEdgeTerminal(edge, "target");
210
234
  if (this.layoutNode(source) && this.layoutNode(target)) {
211
235
  g.setEdge(source, target, {
212
236
  weight: edge.weight || 1,
@@ -215,18 +239,22 @@ export class DagreLayout extends Base {
215
239
  });
216
240
 
217
241
  // create virtual edges from node to node for comboEdges
218
- (comboEdges?.concat(vedges || []))?.forEach((comboEdge: any) => {
242
+ comboEdges?.concat(vedges || [])?.forEach((comboEdge: any) => {
219
243
  const { source, target } = comboEdge;
220
- const sources = comboMap[source]?.collapsed ? [source] : nodeComboMap[source] || [source];
221
- const targets = comboMap[target]?.collapsed ? [target] : nodeComboMap[target] || [target];
244
+ const sources = comboMap[source]?.collapsed
245
+ ? [source]
246
+ : nodeComboMap[source] || [source];
247
+ const targets = comboMap[target]?.collapsed
248
+ ? [target]
249
+ : nodeComboMap[target] || [target];
222
250
  sources.forEach((s: string) => {
223
251
  targets.forEach((t: string) => {
224
252
  g.setEdge(s, t, {
225
253
  weight: comboEdge.weight || 1,
226
254
  });
227
- })
228
- })
229
- })
255
+ });
256
+ });
257
+ });
230
258
 
231
259
  // 考虑增量图中的原始图
232
260
  let prevGraph: DagreGraph | undefined = undefined;
@@ -267,15 +295,15 @@ export class DagreLayout extends Base {
267
295
  dBegin[1] = begin[1] - minY;
268
296
  }
269
297
 
298
+ const isHorizontal = rankdir === "LR" || rankdir === "RL";
270
299
  // 变形为辐射
271
300
  if (radial) {
272
301
  const { focusNode, ranksep, getRadialPos } = this;
273
- const focusId = isString(focusNode) ? focusNode: focusNode?.id;
302
+ const focusId = isString(focusNode) ? focusNode : focusNode?.id;
274
303
  const focusLayer = focusId ? g.node(focusId)?._rank : 0;
275
304
  const layers: any[] = [];
276
- const isHorizontal = rankdir === 'LR' || rankdir === 'RL';
277
- const dim = isHorizontal ? 'y' : 'x';
278
- const sizeDim = isHorizontal ? 'height' : 'width';
305
+ const dim = isHorizontal ? "y" : "x";
306
+ const sizeDim = isHorizontal ? "height" : "width";
279
307
  // 找到整个图作为环的坐标维度(dim)的最大、最小值,考虑节点宽度
280
308
  let min = Infinity;
281
309
  let max = -Infinity;
@@ -286,22 +314,57 @@ export class DagreLayout extends Base {
286
314
  const currentNodesep = nodesepfunc(nodes[i]);
287
315
 
288
316
  if (focusLayer === 0) {
289
- if (!layers[coord._rank]) layers[coord._rank] = { nodes: [], totalWidth: 0, maxSize: -Infinity };
317
+ if (!layers[coord._rank]) {
318
+ layers[coord._rank] = {
319
+ nodes: [],
320
+ totalWidth: 0,
321
+ maxSize: -Infinity,
322
+ };
323
+ }
290
324
  layers[coord._rank].nodes.push(node);
291
325
  layers[coord._rank].totalWidth += currentNodesep * 2 + coord[sizeDim];
292
- if (layers[coord._rank].maxSize < Math.max(coord.width, coord.height)) layers[coord._rank].maxSize = Math.max(coord.width, coord.height);
326
+ if (
327
+ layers[coord._rank].maxSize < Math.max(coord.width, coord.height)
328
+ ) {
329
+ layers[coord._rank].maxSize = Math.max(coord.width, coord.height);
330
+ }
293
331
  } else {
294
332
  const diffLayer = coord._rank - focusLayer!;
295
333
  if (diffLayer === 0) {
296
- if (!layers[diffLayer]) layers[diffLayer] = { nodes: [], totalWidth: 0, maxSize: -Infinity };
334
+ if (!layers[diffLayer]) {
335
+ layers[diffLayer] = {
336
+ nodes: [],
337
+ totalWidth: 0,
338
+ maxSize: -Infinity,
339
+ };
340
+ }
297
341
  layers[diffLayer].nodes.push(node);
298
342
  layers[diffLayer].totalWidth += currentNodesep * 2 + coord[sizeDim];
299
- if (layers[diffLayer].maxSize < Math.max(coord.width, coord.height)) layers[diffLayer].maxSize = Math.max(coord.width, coord.height);
343
+ if (
344
+ layers[diffLayer].maxSize < Math.max(coord.width, coord.height)
345
+ ) {
346
+ layers[diffLayer].maxSize = Math.max(coord.width, coord.height);
347
+ }
300
348
  } else {
301
349
  const diffLayerAbs = Math.abs(diffLayer);
302
- if (!layers[diffLayerAbs]) layers[diffLayerAbs] = { left: [], right: [], totalWidth: 0, maxSize: -Infinity };
303
- layers[diffLayerAbs].totalWidth += currentNodesep * 2 + coord[sizeDim];
304
- if (layers[diffLayerAbs].maxSize < Math.max(coord.width, coord.height)) layers[diffLayerAbs].maxSize = Math.max(coord.width, coord.height);
350
+ if (!layers[diffLayerAbs]) {
351
+ layers[diffLayerAbs] = {
352
+ left: [],
353
+ right: [],
354
+ totalWidth: 0,
355
+ maxSize: -Infinity,
356
+ };
357
+ }
358
+ layers[diffLayerAbs].totalWidth +=
359
+ currentNodesep * 2 + coord[sizeDim];
360
+ if (
361
+ layers[diffLayerAbs].maxSize < Math.max(coord.width, coord.height)
362
+ ) {
363
+ layers[diffLayerAbs].maxSize = Math.max(
364
+ coord.width,
365
+ coord.height
366
+ );
367
+ }
305
368
  if (diffLayer < 0) {
306
369
  layers[diffLayerAbs].left.push(node);
307
370
  } else {
@@ -321,16 +384,30 @@ export class DagreLayout extends Base {
321
384
 
322
385
  // 扩大最大最小值范围,以便为环上留出接缝处的空隙
323
386
  const rangeLength = (max - min) / 0.9;
324
- const range = [ (min + max - rangeLength) * 0.5 , (min + max + rangeLength) * 0.5 ];
387
+ const range = [
388
+ (min + max - rangeLength) * 0.5,
389
+ (min + max + rangeLength) * 0.5,
390
+ ];
325
391
 
326
392
  // 根据半径、分布比例,计算节点在环上的位置,并返回该组节点中最大的 ranksep 值
327
- const processNodes = (layerNodes: any, radius: number, propsMaxRanksep = -Infinity, arcRange = [0, 1]) => {
393
+ const processNodes = (
394
+ layerNodes: any,
395
+ radius: number,
396
+ propsMaxRanksep = -Infinity,
397
+ arcRange = [0, 1]
398
+ ) => {
328
399
  let maxRanksep = propsMaxRanksep;
329
400
  layerNodes.forEach((node: any) => {
330
401
  const coord = g.node(node);
331
402
  radiusMap[node] = radius;
332
403
  // 获取变形为 radial 后的直角坐标系坐标
333
- const { x: newX, y: newY } = getRadialPos(coord![dim]!, range, rangeLength, radius, arcRange);
404
+ const { x: newX, y: newY } = getRadialPos(
405
+ coord![dim]!,
406
+ range,
407
+ rangeLength,
408
+ radius,
409
+ arcRange
410
+ );
334
411
  // 将新坐标写入源数据
335
412
  const i = nodes.findIndex((it) => it.id === node);
336
413
  if (!nodes[i]) return;
@@ -349,7 +426,13 @@ export class DagreLayout extends Base {
349
426
  let isFirstLevel = true;
350
427
  const lastLayerMaxNodeSize = 0;
351
428
  layers.forEach((layerNodes) => {
352
- if (!layerNodes?.nodes?.length && !layerNodes?.left?.length && !layerNodes?.right?.length) return;
429
+ if (
430
+ !layerNodes?.nodes?.length &&
431
+ !layerNodes?.left?.length &&
432
+ !layerNodes?.right?.length
433
+ ) {
434
+ return;
435
+ }
353
436
  // 第一层只有一个节点,直接放在圆心,初始半径设定为 0
354
437
  if (isFirstLevel && layerNodes.nodes.length === 1) {
355
438
  // 将新坐标写入源数据
@@ -365,14 +448,27 @@ export class DagreLayout extends Base {
365
448
 
366
449
  // 为接缝留出空隙,半径也需要扩大
367
450
  radius = Math.max(radius, layerNodes.totalWidth / (2 * Math.PI)); // / 0.9;
368
-
451
+
369
452
  let maxRanksep = -Infinity;
370
453
  if (focusLayer === 0 || layerNodes.nodes?.length) {
371
- maxRanksep = processNodes(layerNodes.nodes, radius, maxRanksep, [0, 1]); // 0.8
454
+ maxRanksep = processNodes(
455
+ layerNodes.nodes,
456
+ radius,
457
+ maxRanksep,
458
+ [0, 1]
459
+ ); // 0.8
372
460
  } else {
373
- const leftRatio = layerNodes.left?.length / (layerNodes.left?.length + layerNodes.right?.length);
374
- maxRanksep= processNodes(layerNodes.left, radius, maxRanksep, [0, leftRatio]); // 接缝留出 0.05 的缝隙
375
- maxRanksep = processNodes(layerNodes.right, radius, maxRanksep, [leftRatio + 0.05, 1]); // 接缝留出 0.05 的缝隙
461
+ const leftRatio =
462
+ layerNodes.left?.length /
463
+ (layerNodes.left?.length + layerNodes.right?.length);
464
+ maxRanksep = processNodes(layerNodes.left, radius, maxRanksep, [
465
+ 0,
466
+ leftRatio,
467
+ ]); // 接缝留出 0.05 的缝隙
468
+ maxRanksep = processNodes(layerNodes.right, radius, maxRanksep, [
469
+ leftRatio + 0.05,
470
+ 1,
471
+ ]); // 接缝留出 0.05 的缝隙
376
472
  }
377
473
  radius += maxRanksep;
378
474
  isFirstLevel = false;
@@ -381,33 +477,54 @@ export class DagreLayout extends Base {
381
477
  g.edges().forEach((edge: any) => {
382
478
  const coord = g.edge(edge);
383
479
  const i = edges.findIndex((it) => {
384
- const source = getEdgeTerminal(it, 'source');
385
- const target = getEdgeTerminal(it, 'target');
480
+ const source = getEdgeTerminal(it, "source");
481
+ const target = getEdgeTerminal(it, "target");
386
482
  return source === edge.v && target === edge.w;
387
483
  });
388
484
  if (i <= -1) return;
389
- if ((self.edgeLabelSpace) && self.controlPoints && edges[i].type !== "loop") {
390
- const otherDim = dim === 'x' ? 'y' : 'x';
391
- const controlPoints = coord?.points?.slice(1, coord.points.length - 1);
485
+ if (
486
+ self.edgeLabelSpace &&
487
+ self.controlPoints &&
488
+ edges[i].type !== "loop"
489
+ ) {
490
+ const otherDim = dim === "x" ? "y" : "x";
491
+ const controlPoints = coord?.points?.slice(
492
+ 1,
493
+ coord.points.length - 1
494
+ );
392
495
  const newControlPoints: Point[] = [];
393
496
  const sourceOtherDimValue = g.node(edge.v)?.[otherDim]!;
394
- const otherDimDist = sourceOtherDimValue - g.node(edge.w)?.[otherDim]!;
497
+ const otherDimDist =
498
+ sourceOtherDimValue - g.node(edge.w)?.[otherDim]!;
395
499
  const sourceRadius = radiusMap[edge.v];
396
500
  const radiusDist = sourceRadius - radiusMap[edge.w];
397
501
  controlPoints?.forEach((point: any) => {
398
502
  // 根据该边的起点、终点半径,及起点、终点、控制点位置关系,确定该控制点的半径
399
- const cRadius = (point[otherDim] - sourceOtherDimValue) / otherDimDist * radiusDist + sourceRadius;
503
+ const cRadius =
504
+ ((point[otherDim] - sourceOtherDimValue) / otherDimDist) *
505
+ radiusDist +
506
+ sourceRadius;
400
507
  // 获取变形为 radial 后的直角坐标系坐标
401
- const newPos = getRadialPos(point[dim], range, rangeLength, cRadius);
508
+ const newPos = getRadialPos(
509
+ point[dim],
510
+ range,
511
+ rangeLength,
512
+ cRadius
513
+ );
402
514
  newControlPoints.push({
403
515
  x: newPos.x + dBegin[0],
404
- y: newPos.y + dBegin[1]
516
+ y: newPos.y + dBegin[1],
405
517
  });
406
518
  });
407
519
  edges[i].controlPoints = newControlPoints;
408
520
  }
409
521
  });
410
522
  } else {
523
+ const layerCoords: Set<number> = new Set();
524
+ const isInvert = rankdir === "BT" || rankdir === "RL";
525
+ const layerCoordSort = isInvert
526
+ ? (a: number, b: number) => b - a
527
+ : (a: number, b: number) => a - b;
411
528
  g.nodes().forEach((node: any) => {
412
529
  const coord = g.node(node)!;
413
530
  if (!coord) return;
@@ -420,17 +537,69 @@ export class DagreLayout extends Base {
420
537
  ndata.y = coord.y! + dBegin[1];
421
538
  // @ts-ignore: pass layer order to data for increment layout use
422
539
  ndata._order = coord._order;
540
+ layerCoords.add(isHorizontal ? ndata.x : ndata.y);
423
541
  });
542
+ const layerCoordsArr = Array.from(layerCoords).sort(layerCoordSort);
543
+
424
544
  g.edges().forEach((edge: any) => {
425
545
  const coord = g.edge(edge);
426
546
  const i = edges.findIndex((it) => {
427
- const source = getEdgeTerminal(it, 'source');
428
- const target = getEdgeTerminal(it, 'target');
547
+ const source = getEdgeTerminal(it, "source");
548
+ const target = getEdgeTerminal(it, "target");
429
549
  return source === edge.v && target === edge.w;
430
550
  });
431
551
  if (i <= -1) return;
432
- if ((self.edgeLabelSpace) && self.controlPoints && edges[i].type !== "loop") {
433
- edges[i].controlPoints = coord?.points?.slice(1, coord.points.length - 1) || []; // 去掉头尾
552
+ if (
553
+ self.edgeLabelSpace &&
554
+ self.controlPoints &&
555
+ edges[i].type !== "loop"
556
+ ) {
557
+ edges[i].controlPoints =
558
+ coord?.points?.slice(1, coord.points.length - 1) || []; // 去掉头尾
559
+
560
+ const sourceNode = self.nodeMap[edge.v];
561
+ const targetNode = self.nodeMap[edge.w];
562
+
563
+ // 酌情增加控制点,使折线不穿过跨层的节点
564
+ if (sourceNode && targetNode) {
565
+ let { x: sourceX, y: sourceY } = sourceNode;
566
+ let { x: targetX, y: targetY } = targetNode;
567
+ if (isHorizontal) {
568
+ sourceX = sourceNode.y;
569
+ sourceY = sourceNode.x;
570
+ targetX = targetNode.y;
571
+ targetY = targetNode.x;
572
+ }
573
+ // 为跨层级的边增加第一个控制点。忽略垂直的/横向的边。
574
+ // 新控制点 = {
575
+ // x: 终点x,
576
+ // y: (起点y + 下一层y) / 2, #下一层y可能不等于终点y
577
+ // }
578
+ if (targetY !== sourceY && sourceX !== targetX) {
579
+ const nextLayerCoord =
580
+ layerCoordsArr[layerCoordsArr.indexOf(sourceY) + 1];
581
+ if (nextLayerCoord) {
582
+ const firstControlPoint = edges[i].controlPoints[0];
583
+ const insertControlPoint = isHorizontal
584
+ ? {
585
+ x: (sourceY + nextLayerCoord) / 2,
586
+ y: firstControlPoint?.x || targetX,
587
+ }
588
+ : {
589
+ x: firstControlPoint?.x || targetX,
590
+ y: (sourceY + nextLayerCoord) / 2,
591
+ };
592
+ // 当新增的控制点不存在(!=当前第一个控制点)时添加
593
+ if (
594
+ !firstControlPoint ||
595
+ firstControlPoint.y !== insertControlPoint.y
596
+ ) {
597
+ edges[i].controlPoints.unshift(insertControlPoint);
598
+ }
599
+ }
600
+ }
601
+ }
602
+
434
603
  edges[i].controlPoints.forEach((point: any) => {
435
604
  point.x += dBegin[0];
436
605
  point.y += dBegin[1];
@@ -446,7 +615,13 @@ export class DagreLayout extends Base {
446
615
  };
447
616
  }
448
617
 
449
- private getRadialPos(dimValue: number, range: number[], rangeLength: number, radius: number, arcRange: number[] = [0, 1]) {
618
+ private getRadialPos(
619
+ dimValue: number,
620
+ range: number[],
621
+ rangeLength: number,
622
+ radius: number,
623
+ arcRange: number[] = [0, 1]
624
+ ) {
450
625
  // dimRatio 占圆弧的比例
451
626
  let dimRatio = (dimValue - range[0]) / rangeLength;
452
627
  // 再进一步归一化到指定的范围上
@@ -456,11 +631,11 @@ export class DagreLayout extends Base {
456
631
  // 将极坐标系转换为直角坐标系
457
632
  return {
458
633
  x: Math.cos(angle) * radius,
459
- y: Math.sin(angle) * radius
634
+ y: Math.sin(angle) * radius,
460
635
  };
461
636
  }
462
637
 
463
638
  public getType() {
464
639
  return "dagre";
465
640
  }
466
- }
641
+ }