算法介绍
我们这里说的布局是指图可视化布局而不是页面布局。Tabular 布局是一个以表格方式放置节点的布局算法。
- 每个表格只能包含一个节点
- 忽略所有节点之间的边
所以这个算法非常适合与其它的可视化布局算法结合使用,来处理没有边的节点不能被处理的问题。
所有的节点会被放置在一个表格中,能够设置表格的相对位置。往往可以用来处理图可视化的数据中没有边连接的节点,让它们能够整齐排列,也可以给定节点的优先级,让它们按指定的顺序排列。
这里提供4种不同的布局模式,可以得到不同的布局效果。
- 单行布局模式
- 单列布局模式
- 固定表格模式
需要传递一个额外的参数,指定表格的列数,传入colCount=7
- 自动表格模式
自动计算表格的行数和列数,不考虑节点的大小,只是让行和列尽可能平衡
算法计算
获取表格的列数
我们一共有4种不同的模式,但其实只有计算的第一步不同,一旦我们知道表格的列数,就能根据节点的数量计算表格的大小。
4种不同的模式也就对应着不同的colCount:
- 单行模式:colCount = nodeList.length
- 单列模式:colCount = 1
- 固定表格模式:colCount = input
- 自动模式:columnCount = Math.ceil(Math.sqrt(nodeListLength))
let colCount = 2;
let nodeListLength = this.nodeList.length;
switch (this.tabularMode) {
case TabularModeOptions["Auto Size"]:
colCount = Math.ceil(Math.sqrt(nodeListLength));
break;
case TabularModeOptions["Fixed Size"]:
var colNum = this.layoutSessionData.columnCount;
// Prevent wasting array space by setting too large a number.
colCount = colNum > nodeListLength ? nodeListLength : colNum;
break;
case TabularModeOptions["Single Row"]:
colCount = nodeListLength;
break;
case TabularModeOptions["Single Column"]:
colCount = 1;
break;
default:
break;
}
return colCount;
知道了表格的列数,我们再根据节点的大小就可以确定每个表格的宽和高。
表格计算
表格计算的思想与excel类似,每列具有相同的宽度,每行具有相同的高度,以此来确定每个表格的大小。
每列根据这一列宽度最大的节点设定这一列的宽度,每行根据这一行节点高度最高的节点设定这一行的高度。
我们就可以用两个一维数组分别来存表格的宽度和高度
const rowWidth = new Array(colCount).fill(0);
const colHeight = new Array(Math.ceil(nodeList.length / colCount)).fill(0);
for (let i = 0; i < nodeList.length; i++) {
rowWidth[i % colCount] = Math.max(rowWidth[i % colCount], nodeList[i].w);
colHeight[Math.floor(i / colCount)] = Math.max(colHeight[Math.floor(i / colCount)], nodeList[i].h);
}
计算节点位置
我们前面只计算了表格的大小,并没有计算表格的坐标,我们在表格的位置默认情况也是节点的位置,我们需要指定第一个节点的位置然后生成其它节点的坐标,因为需要考虑还存在其它节点的布局
// Calculation based on the position of the first node.
if (this.isFirstSiblingAdded) {
nodePosition.push({ x: nodeList[0].x, y: nodeList[0].y, w: rowWidth[0], h: colHeight[0] });
} else {
var firstNodeX, firstNodeY;
if (this.tabularMode === TabularModeOptions["Single Column"]) {
firstNodeX = this.groupNodeBox.x + this.groupNodeBox.w + this.nodeDistX;
firstNodeY = this.groupNodeBox.y;
} else {
firstNodeX = this.groupNodeBox.x;
firstNodeY = this.groupNodeBox.y + this.groupNodeBox.h + this.nodeDistY;
}
nodePosition.push({ x: firstNodeX, y: firstNodeY, w: rowWidth[0], h: colHeight[0] });
}
isFirstSiblingAdded 表示是否有其它节点,true表示没有,就不用考虑其它节点的位置。
groupNodeBox 表示区域存在节点的区域{x,y,w,h}我们表格就需要在其它节点的下面或者右边放置。
最后,就可以根据第一个节点的位置开始放置其它节点。
// Calculate the position of each node in order.
for (let i = 1; i < nodeList.length; i++) {
let x, y, w, h;
if (i % colCount !== 0) {
let preNode = nodePosition[i - 1];
x = preNode.x + preNode.w + this.nodeDistX;
y = preNode.y;
} else {
let preNode = nodePosition[i - colCount];
x = preNode.x;
y = preNode.y + preNode.h + this.nodeDistY;
}
// w, h is the size of the grid in which the node is located rather than the node's own w, h
w = rowWidth[i % colCount];
h = colHeight[Math.floor(i / colCount)];
nodePosition.push({ x, y, w, h });
}
移动节点
我们前面介绍每个节点的表格是由行高和列宽决定的,因此,对于很多节点可能比节点小很多,我们提供了9种不同的相对位置,默认是放置在表格的左上角,可以根据节点的大小和表格的大小进行相对移动。水平方向left,center,right,垂直方向top, center, bottom。提供9种不同的组合。
// Adjust the position of the node in the corresponding grid.
// The default position is the top and left.
for (let i = 0; i < this.nodeList.length; i++) {
let nodeX = nodePosition[i].x;
let nodeY = nodePosition[i].y;
let nodeH = nodePosition[i].h;
let nodeW = nodePosition[i].w;
// The default position is the top and left, Calculate the distance to be moved.
if (this.verticalAlignment === NodePlacementOption["Center"]) {
nodeY += (nodeH - this.nodeList[i].h) / 2.0;
} else if (this.verticalAlignment === NodePlacementOption["Bottom"]) {
nodeY += nodeH - this.nodeList[i].h;
}
if (this.horizontalAlignment === NodePlacementOption["Center"]) {
nodeX += (nodeW - this.nodeList[i].w) / 2.0;
} else if (this.horizontalAlignment === NodePlacementOption["Right"]) {
nodeX += nodeW - this.nodeList[i].w;
}
// Modify the x, y coordinates in the nodePosition reference.
nodePosition[i].y = nodeY;
nodePosition[i].x = nodeX;
}