Tabular 布局算法

430 阅读4分钟

算法介绍

我们这里说的布局是指图可视化布局而不是页面布局。Tabular 布局是一个以表格方式放置节点的布局算法。

  • 每个表格只能包含一个节点
  • 忽略所有节点之间的边
    所以这个算法非常适合与其它的可视化布局算法结合使用,来处理没有边的节点不能被处理的问题。
    所有的节点会被放置在一个表格中,能够设置表格的相对位置。往往可以用来处理图可视化的数据中没有边连接的节点,让它们能够整齐排列,也可以给定节点的优先级,让它们按指定的顺序排列。

图片.png

这里提供4种不同的布局模式,可以得到不同的布局效果。

  • 单行布局模式

图片.png

  • 单列布局模式
    图片.png
  • 固定表格模式
    需要传递一个额外的参数,指定表格的列数,传入colCount=7
    图片.png
  • 自动表格模式
    自动计算表格的行数和列数,不考虑节点的大小,只是让行和列尽可能平衡
    图片.png

算法计算

获取表格的列数

我们一共有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类似,每列具有相同的宽度,每行具有相同的高度,以此来确定每个表格的大小。
每列根据这一列宽度最大的节点设定这一列的宽度,每行根据这一行节点高度最高的节点设定这一行的高度。 图片.png 我们就可以用两个一维数组分别来存表格的宽度和高度

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种不同的组合。 图片.png

// 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;
}