浅析图布局:常见可视化布局算法原理

4,577 阅读19分钟

一、介绍

图可视化是将数据转换成图形或图像在屏幕上显示出来,再进行交互处理的理论、方法和技术。数据在经过图可视化的方式展示后能够辅助用户去分析复杂的关系数据,从而发现数据中蕴含的价值。图布局则是图可视化中重要的基石,对可视化图进行合理的布局可以帮助我们快速分析,准确定位问题。如果没有对图进行合理的布局,那么用户就很难从图中提炼出想要的信息,甚至有时候还会误导用户作出错误的判断。
图的定义:图在数学上通常用 G = (V, E) 来表示,其中 V 是顶点的集合,E 是边的集合,且每条边 e ∈ E 都连接两个顶点 < v i ,v j >,且边 e 通常由 < v i ,v j > 来存储表示,对于每个顶点 v i来说,布局的最终目的是确定 v i 在画布上的位置 (x i , y i )。图分为有向图和无向图。
本次分享以antv.g6为实现方式,以复杂逻辑情况下数据库ER图为例探索不同布局方案的原理及可行性,讨论这些布局算法的实现思路以及适用情况和应用场景。

二、布局方案

一、层次布局dagre

(1)效果

image.png

image.png

(2)算法原理

将无向图转成有向图
第一阶段:去环。
①DFS去环:每个节点从自己的每条出边开始深度优先遍历,如果出边e1的遍历路径中不存在当前节点,说明当前链路不存在环路。如果出边e1的遍历路径中存在当前节点,则说明存在环。通过去掉最后环路的最后一条边,达到去环的效果。
②贪心算法去环:优先反转那些同时存在于多个环路中的边,尽可能的少反转边。
对于给定的一张图,按照一定顺序从左到右排列节点,使得边的方向尽量是从左到右的,那么从右到左的边则成为可能形成环路的边,即为需要反转的边。从而将求最少反转环的问题,转换成求一个节点序列。

image.png


Dagre的做法:逆转第一个出度-入度 max节点的入边。即在上述步骤3的时候,就移除了d->c.
第二阶段:层级划分。
①最长路径算法:对于有向无环图,每个节点都返回一个rank属性。
image.png
②紧致树算法:
先用最长路径算出初始层级,然后调整松弛边的长度。松弛边定义:边的长度大于其实际所需长度(minlen)。

  1. 定义边的松弛度为(边长-边的minlen)。
  2. 把目前已经紧致的节点添加到tightTree中,然后从松弛度最小的边开始逐步处理。
  3. 如果松弛边的源节点在tightTree,将边的目标节点层级向上移动delta;
  4. 如果松弛边的源节点不在tightTree,将边的源节点层级向下移动delta;
  5. 直至所有的节点都被加到tightTree中。

③networks simplex算法

  1. 先将图中紧致的边及其相关的节点加入到树中,构建一颗紧致生成树。
  2. 分配给起始节点一个层级,然后从起始节点出发,找到邻近的节点,然后根据其与起始节点的上下游关系,计算连通边的最小长度。
  3. 直到所有的节点都有一个默认层级。
  4. 计算每条边的切割值。切割值的计算:移除该边,将生成树分为两个区域,根据边的方向,分为头部区域v和尾部区域w,那么该边的切割值就等于,尾部区域w到头部区域v的所有连接边的weight之和(注:如果边的方向是从头部到尾部的,那么该边的weight即为负值)。
  5. 切割值为负的边,即为需要调整的边。调整方式:加长该边的长度。
  6. 替换生成树的边,构成新的生成树。
  7. 根据新的生成树,重新调整层级。

切割值的计算:
image.png

image.png


切割值 = 边的weight + 节点其他边切割值之和 + 节点的出边weight - 节点的入边weight

image.png


第三阶段:同层级内节点排序。


认为图中的垂直的边越多,边的交叉越少

  1. 返回一个层级矩阵,每一个层级对应一个数组,每一层按照节点的顺序进行排序。
  2. 按照节点在所在层级数组中的位置,赋予其初始order。
  3. 计算每一层级中节点的barycenter和weight
  4. 根据barycenter的值,调整节点顺序。
  5. 根据调整后的结果,重新计算出层级矩阵。
  6. 根据层级矩阵,算出交叉边的个数。与上一次计算出的交叉边数作比较,交叉少的排序方案。

barycenter:计算每个节点的重心值。

image.png


调整节点顺序:简单理解成当存在两个节点O(v1)>O(v2),则必须存在B(v1)>B(v2),如果不符合条件,则交换节点位置。

image.png
第四阶段:绘图
一个可读性较高的图应该满足:

  1. 边尽量的短
  2. 上下层的节点位置要平衡,长边尽量不要出现拐点。

核心思想:

  1. 垂直对齐。尝试将每个顶点与其中上邻或中下邻对齐,然后以最左或最右的方式解决对齐冲突(贪婪算法)。垂直对齐的部分,需要尽可能的将每个层级的顶点与上一个层级相连的顶点对齐。
  2. 水平压缩。将对齐的顶点约束为获得相同的水平坐标,并且将所有顶点放置在对齐的首选水平方向上,使其尽可能靠近下一个顶点。我们将框图划分为多个类,每一个类应用最长路径分层算法。

image.png

image.png

(3)适用情况

Dagre(有向无环图)布局其实有点类似于树形布局,对于任何有向树图必定是有向无环图,但是有向无环图未必是有向树图,由于有向无环图有其严密的拓扑性质,使其具有很强的流程表达能力。基于这一特点,在很多需要对零散化任务进行组织和控制的场景中。
Dagre算法将图划分了层级、同层节点排序(解决节点间线段交叉的问题)、绘图时保持图的平衡对图进行垂直对齐和水平压缩,适用于大多数ER图场景,对于连线较小的图可以有一个较好的布局展示。

image.png

2、力导布局

(1)效果

image.png


image.png

(2)算法原理

力导布局最早是被 Eades 提出的,之后被很多研究人员重新定义和改进。力导向算法是指通过对每个节点的计算,算出引力和排斥力综合的合力,再由此合力来移动节点的位置。力导布局根据自然界中电子直接互相作用的原理来实现的。加上电子作用力(库伦公式)和弹簧力(胡克定律)的力导向结果更接近自然界的结果。
库伦定理:真空中两个静止的点电荷之间的相互作用力同它们的电荷量的乘积成正比,与它们的距离的二次方成反比,作用力的方向在它们的连线上,同名电荷相斥,异名电荷相吸。
胡克定律:F=-kx或△F=-k·Δx(k为劲度系数)

image.png

(e为q1到q2的矢径)

image.png

image.png


实现逻辑:

  1. 设置点数据nodes, 链接数据links。
  2. 对点进行随机定位。
  3. 渲染视图
  4. 执行力算法计算位置,渲染视图
const springLength = 40; //弹簧静止长度
const springStrength = 0.1; //弹性系数
const repulsionStrength = 1500; //库伦常数

function forceDirected_simple(graph) {
  for (let node of graph) {
    for (let other of graph) {
      if (other === node) continue;
      let apart = other.pos.minus(node.pos);
      let distance = Math.max(1, apart.length); //计算节点间距离,最小值设为1
      let forceSize = -repulsionStrength / (distance * distance); //洛伦兹力大小
      if (node.hasEdge(other)) {
        forceSize += (distance - springLength) * springStrength; //弹簧力
      }
      let normalized = apart.times(1 / distance);
      node.pos = node.pos.plus(normalized.times(forceSize)); //调整节点位置
    }
  }
}

(3)适用情况

力导布局的优势在于由于互斥力的存在,可以减少节点之间的重叠,但实际上当节点过多时,仍然会出现节点重叠的情况。

  1. 节点的位置没有重叠,不会互相遮挡;
  2. 节点位置总体上趋近去画布中心;
  3. 有关联的节点通过连线相连,并且相互靠拢;
  4. 节点的位置最终会稳定下来;

由于力的作用具有相对性,力导布局适用于无向图。力导布局受到向图节点大小的影响,图中线的长度、角度等无法映射到力导的变量上,因此链路的长短是根据算法计算出来的,不能指定。力导向图不适合用来表达有具体关系举例的场景。比如说每个节点代表一个省份,而且要按照真实地理位置布置好节点的位置,再用链路链接,表达各省间的某种关系。这种场景应该用一般网络图或者定制网络图,不适合用力导向算法来做布局。
图的位置权重节点变的数量以及节点的大小有关。对于边数较多或节点权重(大小)差异较大的情况会有更明显的效果。

3、环形布局

(1)效果

image.png


image.png

(2)算法

设置圆的半径、开始角度和结束角度、要划分的区域数。
遍历1-n,计算每一个i值对应的圆形的半径,计算出点旋转的角度。根据半径和角度求出布局节点对应的x、y的位置值。

let radius = self.radius
let startRadius = self.startRadius
let endRadius = self.endRadius
const divisions = self.divisions
const startAngle = self.startAngle
const endAngle = self.endAngle
const angleStep = (endAngle - startAngle) / n
const angleRatio = self.angleRatio
const astep = angleStep * angleRatio

const ordering = self.ordering
let layoutNodes = []
if (ordering === 'topology') {
	// layout according to the topology
	layoutNodes = self.topologyOrdering()
} else if (ordering === 'degree') {
	// layout according to the descent order of degrees
	layoutNodes = self.degreeOrdering()
} else {
	// layout according to the original order in the data.nodes
	layoutNodes = nodes
}

const clockwise = self.clockwise
const divN = Math.ceil(n / divisions) // node number in each division
//遍历
for (let i = 0; i < n; ++i) {
	//设置圆形的半径
	let r = radius
	if (!r && startRadius !== null && endRadius !== null) {
		r = startRadius + (i * (endRadius - startRadius)) / (n - 1)
	}
	if (!r) {
		r = 10 + (i * 100) / (n - 1)
	}
	//计算出点旋转的角度
	//divN每一个弧度中的节点数,astep为旋转过的角度
	let angle =
			startAngle +
			(i % divN) * astep +
			((2 * Math.PI) / divisions) * Math.floor(i / divN)
	if (!clockwise) {
		angle =
			endAngle -
			(i % divN) * astep -
			((2 * Math.PI) / divisions) * Math.floor(i / divN)
	}
	//计算出节点的x、y的位置
	layoutNodes[i].x = center[0] + Math.cos(angle) * r
	layoutNodes[i].y = center[1] + Math.sin(angle) * r
	layoutNodes[i].weight = degrees[i]
}

if (self.onLayoutEnd) self.onLayoutEnd()

return {
	nodes: layoutNodes,
	edges: this.edges,
}

(3)适用情况

圆形布局是将所有节点先进行一定的排序,然后再按序排列在一个圆环上,这种布局的好处是通过自定义排序后的圆形布局,我们可以快速分析出想要的结果。排序的方式可以根据节点的出入度进行排序,也可以根据有向无环图进行拓扑排序。
适用于查找关联关系较多的关键节点场景,很多应用程序通过环形图来增强表现效果,例如环形布局技术应用在计算机通信、计算机网络拓扑结构或者社交网络中,以显示信息结构的集群特点。

4、网格布局grid

(1)效果

image.png


image.png

(2)算法原理

容器里面的水平区域称为"行"(row),垂直区域称为"列"(column),划分网格的线,称为"网格线"(grid line)。水平网格线划分出行,垂直网格线划分出列。

image.png
执行流程

  1. 对节点进行排序(可以定义degree或者value)
  2. 计算网格布局的rows和cols。width/height * splits^2 = cells,splits为需要切分的次数
  3. 计算节点布局后的位置
//截取部分grid布局代码
public execute() {
	//排序
	if (
		sortBy === "degree" ||
		!isString(sortBy) ||
		(layoutNodes[0] as any)[sortBy] === undefined
	) {
		sortBy = "degree";
		if (isNaN(nodes[0].degree)) {
			const values = getDegree(layoutNodes.length, nodeIdxMap, edges);
			layoutNodes.forEach((node, i) => {
				node.degree = values[i];
			});
		}
	}
	// sort nodes by value
	layoutNodes.sort(
		(n1, n2) => (n2 as any)[sortBy] - (n1 as any)[sortBy]
	);
	//获取/计算网格布局的rows和cols
	// if rows or columns were set in self, use those values
	if (oRows != null && oCols != null) {
		self.rows = oRows;
		self.cols = oCols;
	} else if (oRows != null && oCols == null) {
		self.rows = oRows;
		self.cols = Math.ceil(self.cells / self.rows);
	} else if (oRows == null && oCols != null) {
		self.cols = oCols;
		self.rows = Math.ceil(self.cells / self.cols);
	} else {
		// otherwise use the automatic values and adjust accordingly
		// width/height * splits^2 = cells where splits is number of times to split width
		self.splits = Math.sqrt((self.cells * self.height) / self.width);
		self.rows = Math.round(self.splits);
		self.cols = Math.round((self.width / self.height) * self.splits);
	}
	self.cellWidth = width / self.cols;
	self.cellHeight = height / self.rows;
	
	//计算节点布局后的位置
	if (preventOverlap || paramNodeSpacing) {
		const nodeSpacing: Function = getFuncByUnknownType(10, paramNodeSpacing);
		const nodeSize: Function = getFuncByUnknownType(30, paramNodeSize, false);
		layoutNodes.forEach((node) => {
			if (!node.x || !node.y) {
				// for bb
				node.x = 0;
				node.y = 0;
			}
			
			const [nodew = 30, nodeh = 30] = nodeSize(node);		
			const p = nodeSpacing !== undefined ? nodeSpacing(node) : preventOverlapPadding;
			const w = nodew + p;
			const h = nodeh + p;	
			self.cellWidth = Math.max(self.cellWidth, w);
			self.cellHeight = Math.max(self.cellHeight, h);
		});
	}
	
}

(3)适用情况

节点按一定排序网状排开,适应于分层网络,便于看清整体层次。
优点:节点排布整齐,划分清晰。
缺点:可能会存在线的重合问题,用户点击线查看详情上有困难。

5、辐射布局radial

(1)效果

image.png

(2)算法

执行流程:

  1. 设置focusNode。默认值为nodes[0]。
  2. 获取最短距离矩阵,并求出图的最大距离distance
  3. 计算每一个节点距离中心节点的半径
  4. 调用mds的第一次迭代计算node的位置。
//设置focusnode
// default focus node
if (!focusNode) {
	focusNode = nodes[0];
	self.focusNode = focusNode;
}
// the index of the focusNode in data
let focusIndex = getIndexById(nodes, focusNode.id);
if (focusIndex < 0) focusIndex = 0;
self.focusIndex = focusIndex;

//获取最短距离矩阵,并求出图的最大距离distance
// the graph-theoretic distance (shortest path distance) matrix
const adjMatrix = getAdjMatrix({ nodes, edges }, false);
const D = floydWarshall(adjMatrix);
const maxDistance = self.maxToFocus(D, focusIndex);
// replace first node in unconnected component to the circle at (maxDistance + 1)
self.handleInfinity(D, focusIndex, maxDistance + 1);
self.distances = D;

//计算每一个节点距离中心节点的半径
// the maxRadius of the graph
const maxRadius = semiHeight > semiWidth ? semiWidth : semiHeight;
const maxD = Math.max(...focusNodeD);
// the radius for each nodes away from focusNode
const radii: number[] = [];
focusNodeD.forEach((value, i) => {
	if (!self.unitRadius) {
		self.unitRadius = maxRadius / maxD;
	}
	radii[i] = value * self.unitRadius;
});
self.radii = radii;
//初始化mds的initial positions
//省略
//调用mds迭代计算每个节点的x、y位置。
self.oneIteration(param, positions, radii, eIdealDis, W);

(3)适用情况

Radial 布局是将图布局成辐射状的布局方法。以一个 focusNode 为中心,其余节点按照与 focusNode 的度数关系排列在不同距离的环上。距离 focusNode 一度的节点布局在与其最近的第一个环上,距离 focusNode 二度的节点布局在第二个环上,以此类推。辐射状布局适用于探索关注点的一度关系、二度关系、...等。
ER图的属于无向图,无法通过节点进行层级的辐射,出现了所有节点都在第二层。可能的解决方案:将图转化成树(e.g.最小生成树算法),对数进行辐射布局。认为对于复杂、边数较多的ER图情况下较难划分出清晰的层级,故认为辐射布局不适合ER图讨论的情况。

6、Fruchterman布局

(1)效果

image.png

(2)算法

Fruchterman-Reingold算法属于力导布局的一种,其本质是将之前Eades的布点算法中的基于胡克定律模型进行了改进,使用了库伦斥力并且聚焦在最近相邻节点之间的能量模型,利用模拟退火等优化策略,结合美学标准对整体进行减少线交叉及整体均匀布局。
有边连接的节点应该互相靠近;节点间不能离得太近。FR算法建立在粒子物理理论的基础上,将图中的节点模拟成原子,通过模拟原子间的力场来计算节点间的位置关系。算法通过考虑原子间引力和斥力的互相作用,计算得到节点的速度和加速度。依照类似原子或者行星的运动规律,系统最终进入一种动态平衡状态。
弹簧模型:

image.png
能量模型:

image.png
引力:

image.png
斥力:

image.png

image.png
布局算法伪代码:

    设置节点的初始速度为(0,0)
    设置节点的初始位置为任意但不重叠的位置
    开始循环
    总动能:=0   //所有粒子的总动能为0
    对于每个节点i
       净力f:=(0,0)
    对于出该节点外的每个节点j
       净力f:=净力f + j节点对应i节点的库伦斥力
    下一个节点j+1

    对于该节点上的每个弹簧s
       净力f:= 净力f+弹簧对该节点的胡克弹力
    下一个弹簧s+1

    //如果没有阻尼衰减的话,整个系统将一直运动下去
    该节点速度:=(该节点速度 + 步长 * 净力) * 阻尼
    该节点位置:= 该节点位置 + 步长 * 该节点速度
    总动能:=总动能 + 该节点质量 * (该节点速度)^2
    下一个节点i+1
// 截取了antg6部分代码
// 计算斥力 并移动节点位置
  private calRepulsive(nodes: INode[], displacements: Point[], k2: number) {
    nodes.forEach((v, i) => {
      displacements[i] = { x: 0, y: 0 };
      nodes.forEach((u, j) => {
        if (i === j) {
          return;
        }
        if (!isNumber(v.x) || !isNumber(u.x) || !isNumber(v.y) ||!isNumber(u.y))
          return;
        let vecX = v.x - u.x;
        let vecY = v.y - u.y;
        let vecLengthSqr = vecX * vecX + vecY * vecY;
        if (vecLengthSqr === 0) {
          vecLengthSqr = 1;
          const sign = i > j ? 1 : -1;
          vecX = 0.01 * sign;
          vecY = 0.01 * sign;
        }
        const common = k2 / vecLengthSqr;
        displacements[i].x += vecX * common;
        displacements[i].y += vecY * common;
      });
    });
  }

// 计算引力 并移动节点位置
  private calAttractive(edges: Edge[], displacements: Point[], k: number) {
    edges.forEach(e => {
      if (!e.source || !e.target) return;
      const uIndex = this.nodeIdxMap[e.source];
      const vIndex = this.nodeIdxMap[e.target];
      if (uIndex === vIndex) {
        return;
      }
      const u = this.nodeMap[e.source];
      const v = this.nodeMap[e.target];
      if (!isNumber(v.x) || !isNumber(u.x) || !isNumber(v.y) || !isNumber(u.y))
        return;
      const vecX = v.x - u.x;
      const vecY = v.y - u.y;
      const vecLength = Math.sqrt(vecX * vecX + vecY * vecY);
      const common = (vecLength * vecLength) / k;
      displacements[vIndex].x -= (vecX / vecLength) * common;
      displacements[vIndex].y -= (vecY / vecLength) * common;
      displacements[uIndex].x += (vecX / vecLength) * common;
      displacements[uIndex].y += (vec
																	Y / vecLength) * common;
    });
  }

  public run() {
		//计算节点重力
      nodes.forEach((n, j) => {
        if (!isNumber(n.x) || !isNumber(n.y)) return;
        const gravityForce = 0.01 * k * gravity;
        displacements[j].x -= gravityForce * (n.x - center[0]);
        displacements[j].y -= gravityForce * (n.y - center[1]);
      });
	 //move
      nodes.forEach((n, j) => {
        if (!isNumber(n.x) || !isNumber(n.y)) return;
        const distLength = Math.sqrt(
          displacements[j].x * displacements[j].x +
            displacements[j].y * displacements[j].y
        );
        if (distLength > 0) {
          // && !n.isFixed()
          const limitedDist = Math.min(
            maxDisplace * (speed / SPEED_DIVISOR),
            distLength
          );
          n.x += (displacements[j].x / distLength) * limitedDist;
          n.y += (displacements[j].y / distLength) * limitedDist;
        }
      });
    }
	}

(3)适用情况

数据优化版的力导布局,和力导布局的适用情况相似。

7、同心圆布局concentric

(1)效果

image.png

(2)算法

Concentric 布局为同心圆布局,用户可以指定节点某个属性为排序依据(默认为节点度数 degree),该属性值越高,则该节点布局后的位置中心。

image.png
布局执行流程:

  1. 获取节点的度数
  2. 将节点依据度数排序
  3. 计算节点对应的层级
  4. 计算层级的对应位置
  5. 计算每一个层级的度量值
  6. 计算层中每一个节点的位置
//截取了部分流程源码
public execute(){
	// get the node degrees
	if (
		self.sortBy === "degree" ||
		!isString(self.sortBy) ||
		(layoutNodes[0] as any)[self.sortBy] === undefined
	) {
		self.sortBy = "degree";
		if (!isNumber(nodes[0].degree)) {
			const values = getDegree(nodes.length, indexMap, edges);
			layoutNodes.forEach((node, i) => {
				node.degree = values[i];
			});
		}
	}
	
	// sort nodes by value
	layoutNodes.sort(
		(n1: INode, n2: INode) =>
		(n2 as any)[self.sortBy] - (n1 as any)[self.sortBy]
	);
	
	self.maxValueNode = layoutNodes[0];
	//maxLevelDiff每一层同心值的求和。默认值为maxValue/4 ,其中maxValue为最大的排序依据的属性值(默认为degree)
	self.maxLevelDiff =
		self.maxLevelDiff || (self.maxValueNode as any)[self.sortBy] / 4;
	
	// put the values into levels
	const levels: any[] = [[]];
	let currentLevel = levels[0];
	layoutNodes.forEach((node) => {
		if (currentLevel.length > 0) {
			const diff = Math.abs(
				currentLevel[0][self.sortBy] - (node as any)[self.sortBy]
			);
			if (self.maxLevelDiff && diff >= self.maxLevelDiff) {
				currentLevel = [];
				levels.push(currentLevel);
			}
		}
		currentLevel.push(node);
	});
	
	// create positions for levels
	if (!self.preventOverlap) {
		// then strictly constrain to bb
		const firstLvlHasMulti = levels.length > 0 && levels[0].length > 1;
		const maxR = Math.min(self.width, self.height) / 2 - minDist;//最大半径
		const rStep = maxR / (levels.length + (firstLvlHasMulti ? 1 : 0));//每一层的半径step值
		minDist = Math.min(minDist, rStep);
	}
	
	// calculate the node positions
	levels.forEach((level) => {
		const dTheta = level.dTheta;
		const rr = level.r;
		level.forEach((node: INode, j: number) => {
			const theta = self.startAngle + (self.clockwise ? 1 : -1) * dTheta * j;
			node.x = center[0] + rr * Math.cos(theta);
			node.y = center[1] + rr * Math.sin(theta);
		});
	});
}

(3)适用情况

适用与ER图中连线较多的情况,且节点度数(或自定义的度量值)能够区分出较清晰的“层级关系”。同心圆布局将重要的度量节点放置在布局中心,按重要性关系层级扩展。

三、适用情况判断

考虑到布局大多数情况下数据没有特别复杂,系统使用的层次布局可以满足大多数ER图的布局要求,且有较好的布局效果。
部分情况下的ER图包含很多孤立节点,与其他节点间不存在ER关系,采用层级布局将孤立节点单独排布。有较清晰的布局效果。

针对于关系特别多,ER图连线较为复杂的情况,可以考虑采用力导布局/环形布局来展现图的效果。需要探讨的是什么情况符合“关系特别多且连线复杂”,需要适配更复杂的布局方案。上文中采用的案例包含10个节点和38个节点间关系,采用层级布局就呈现出相对混乱的效果。

image.png
①可以考虑边-节点比值来初步判断,当2边数/节点数(节点的平均度数)大于一定阈值时,认为ER图关系较为复杂,采用其他的布局方式呈现效果。计算边数/节点值时需要排除掉孤立节点,考虑到孤立节点对于关系图的布局没有影响,但会减小边数/节点比。
②每个节点的度数是一个可以参考的数值,2
边数/节点为节点的平均度数,除此还可以考虑节点度数的分布情况,例如是否存在某个节点度数非常高,其余节点度数较小的情况,即有一个节点的权值特别高,应将其布局到中心的位置,这种情况更适合用同心圆布局或力导布局,如果节点平均度数高且节点度数分布平均,这种情况更适合用环形布局或者网格布局。
案例中节点的度数分布如下,节点平均度数为7.6,节点总数为10,该数值较高,可判断为复杂场景。

//边的矩阵图
[[0,1,1,1,1,1,1,1,1,1],
[1,0,1,1,1,1,1,1,1,1],
[1,1,0,0,0,0,0,1,1,1],
[1,1,0,0,0,0,1,1,1,1],
[1,1,0,0,0,1,1,1,1,1],
[1,1,0,0,1,0,0,1,1,1],
[1,1,0,1,1,0,0,1,1,1],
[1,1,1,1,1,1,1,0,1,1],
[1,1,1,1,1,1,1,1,0,1],
[1,1,1,1,1,1,1,1,1,0]]、
//节点的度数
cdm_huiyuan_t1: 9
cdm_huiyuan_t2: 9
ods_huiyuan_t1: 5
ods_huiyuan_t5: 6
ods_huiyuan_t4: 7
ods_huiyuan_t3: 6
ods_huiyuan_t2: 7
ads_huiyuan_t2: 9
ads_huiyuan_t1: 9
ads_huiyuan_t1_fk: 9

四、其他优化方式

压实操作

基于约束图的压实

(1)最小距离约束(分隔约束)
将节点之间的关联,构建成一个约束图 。
两个元件之间的最小距离,需要大于指定值。最小距离为b,节点的最小宽度为a,则需要满以下约束条件:

image.png

  • 对于每个条件,换成为约束图的一条边
  • 新增一个额外的虚拟节点,0表示节点的x坐标是0,从而所有的节点都在该节点的右侧。
  • 把所有的不等式条件约束,全都转化为最小距离约束,就会生成一个dag。

image.png
(2)最大距离约束(关联约束)
两个元件的距离,需要保持在一个指定范围内,约束图中用反向边来表示。

保持分隔约束和关联约束的条件下迭代移动节点的位置。

基于虚拟网格的压实

节点的布局是在网格上进行的。每一个元件都在网格线上,两条平行相邻轴线之间的距离为,两个轴线上元素之间的最大间距。通过移动轴线距离,来达到压实的目的。

image.png g更进一步,即同一行或同一列上的节点可以进行拆分,拆分到不同的行列上。从而增加图的紧密性。

image.png

参考内容

图可视化之图布局:
juejin.cn/post/692018…
segmentfault.com/a/119000003…
dagre图绘制原理:www.researchgate.net/publication…
developer.aliyun.com/article/780…
力导布局/Fruchterman布局绘制原理:
juejin.cn/post/687414…
segmentfault.com/a/119000001…
www.mathe2.uni-bayreuth.de/axel/papers…
环形布局绘制原理:
www.sciencedirect.com/science/art…
g6源码结构:
github.com/antvis/layo…(布局github源码地址)