!!!###!!!title=3.3 Row Height, Column Width Calculation——VisActor/VTable Contributing Documents!!!###!!!!!!###!!!description=---title: 3.3 Row Height, Column Width Calculation key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM--- !!!###!!!

Background of the Requirement

During the rendering process of the table, cells are generated, but unlike the native DOM, Canvas cells cannot be expanded by content. We must know the row height and column width of the content in order to dynamically adjust the width and height of the cells based on the row height and column width.

Solution

Suppose we have a piece of text \r

We want to calculate its width and height through Canvas, the usual operation is like this: \r

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.font = ....
let measure = ctx.measureText("@Visactor/VTable");
const { 
  actualBoundingBoxLeft,
  actualBoundingBoxRight,
  actualBoundingBoxAscent,
  actualBoundingBoxDescent, 
  width 
} = measureText;
const realWidth = Math.max(actualBoundingBoxLeft + actualBoundingBoxRight, width);
const height = actualBoundingBoxAscent + actualBoundingBoxDescent;
console.log(realWidth,height);    

Limitations

However, this method can only obtain the most basic width and height, but there are many other influencing factors within the VTable, such as wrapping operations, which will affect the final calculation of width and height. So how to accurately calculate row height and column width for different configurations becomes a challenge. Next, let's see how the VTable operates internally. \r

Bounding Box

Before introducing the specific calculation logic, it is necessary to first introduce the concept of bounding boxes. \r

In the field of computer and graphic vision, a bounding box is a container that encloses a group of objects. By wrapping complex objects in simple containers, it is possible to approximate the shape of complex geometries with simple bounding boxes, which can improve computational efficiency. Additionally, simple objects are generally easier to check for overlap with each other. \r

In VRender, AABBBounds is implemented. AABBBounds is a relatively simple type of bounding box with poor tightness. Within VTable, each basic primitive maintains its own AABBBounds, which can be used to calculate width and height.

Record the coordinates of the four vertices of the current bounding box in the AABBBounds instance. With the concept of a bounding box, it becomes much more convenient to implement functions such as width and height calculation, rotation, and cropping. \r

For example, if we want to get the height of a piece of text, we can directly calculate it using this.y2 - this.y1.

// VisActor/VUtil/blob/main/packages/vutils/src/data-structure/bounds.ts
export class Bounds implements IBounds {
  // 默认初始值是Number.MAX_VALUE
  x1: number;
  y1: number;
  x2: number;
  y2: number;

  constructor(bounds?: Bounds) {
    if (bounds) {
      this.setValue(bounds.x1, bounds.y1, bounds.x2, bounds.y2);
    } else {
      this.clear();
    }
  }
  // ...
  rotate(angle: number = 0, x: number = 0, y: number = 0) {
    const p = this.rotatedPoints(angle, x, y);
    return this.clear().add(p[0], p[1]).add(p[2], p[3]).add(p[4], p[5]).add(p[6], p[7]);
  }
  width(): number {
    if (this.empty()) {
      return 0;
    }
    return this.x2 - this.x1;
  }
  height(): number {
    if (this.empty()) {
      return 0;
    }
    return this.y2 - this.y1;
  }    

Related Documents

Basic Width and Height Calculation

The underlying calculation of the width and height of the VTable relies on the AABBBounds provided by Visactor/Vutils to complete the calculation. \r

  • Accurate calculation of text width and height \r

First, use getTextBounds to get the bounding box corresponding to the text, and then use the internal width and height to get the dimensions. \r

  // VisActor/VUtil/blob/main/packages/vutils/src/graphics/text/measure/textMeasure.ts
  /** 精确计算文本宽高 */
  fullMeasure(text: TextMeasureInput): ITextSize {
    if (isNil(text)) {
      return { width: 0, height: 0 };
    }
    if (isNil(this._option.getTextBounds) || !this._notSupportVRender) {
      return this.measureWithNaiveCanvas(text); // 降级
    }
    const { fontFamily, fontSize, fontWeight, textAlign, textBaseline, ellipsis, limit, lineHeight } = this.textSpec;
    let size: ITextSize;
    //...
     const bounds = this._option.getTextBounds({
        text,
        fontFamily,
        fontSize,
        fontWeight,
        textAlign,
        textBaseline,
        ellipsis: !!ellipsis,
        maxLineWidth: limit || Infinity,
        lineHeight
      });
     size = { width: bounds.width(), height: bounds.height() };
     //...
    return size;    

  • Use native Canvas to calculate width and height

When encountering situations where VRender is not supported, native Canvas will be used to perform calculations.

// VisActor/VUtil/blob/main/packages/vutils/src/graphics/text/measure/textMeasure.ts
  protected _measureWithNaiveCanvas(text: string): ITextSize {
    if (!this.initContext()) {
      return this._quickMeasureWithoutCanvas(text); // 降级
    }
    const metrics = this._context!.measureText(text);
    const { fontSize, lineHeight } = this.textSpec;
    return {
      width: metrics.width,
      height: (lineHeight as number) ?? fontSize,
      fontBoundingBoxAscent: metrics.fontBoundingBoxAscent,
      fontBoundingBoxDescent: metrics.fontBoundingBoxDescent
    };
  }
    

Column Width Calculation

Let's first look at the calculation of column width \r

Column Width Calculation Mode

Table column width calculation modes, there are three configurations below:

  • 'standard': Use the width attribute specified width as the column width.

  • 'adaptive': Use the width of the table container to allocate column widths.

  • 'autoWidth': Automatically calculate the column width based on the content in the column header and body cells, ignoring the width attribute setting.

Calculation Process

Impact in Different Calculation Modes

To calculate the column width of an entire column, it is not enough to obtain the column width of a single row. Instead, you need to find the maximum column width in the entire column (this has different effects under different calculation modes).

Assuming there are the following three cells, the content length of the three cells is not the same, you cannot randomly take the width of one cell as the column width for this column, there must be a definite width. \r

In VTable, different calculation modes for column widths have different logic for adjusting column widths: \r

  • standard

Under standard width, all widths will follow the default configuration; \r

For example, the column width of the three cells above will be uniformly adjusted to 80px; \r

  • autoWidth

In autoWidth mode, the width of the entire column will be adjusted based on the longest column among all columns. It should be noted that the maximum column width cannot exceed limitMaxAutoWidth;

  • adaptive

In the container width adaptation mode, the column width is first calculated based on autoWidth, and then the column width is proportionally scaled according to the ratio of the container column width to the actual column width. \r

Multi-column Width Calculation

Here is the overall flowchart with multiple column widths

  • computeColsWidth (packages\vtable\src\scenegraph\layout\compute-col-width.ts)

Internally, it will traverse by column and call computeColWidth for each column to calculate the width of the column separately.

Single Column Width Calculation

  • computeColWidth
Pre-process

In the process of obtaining the overall column width, each column is traversed to get the width of that column. Depending on the columnWidthComputeMode, different rows are involved in calculating the width of the column.

Source Code
// packages\vtable\src\scenegraph\layout\compute-col-width.ts
export function computeColWidth(
  col: number,
  startRow: number,
  endRow: number,
  table: BaseTableAPI,
  forceCompute: boolean = false *//forceCompute如果设置为true 即便不是自动列宽的列也会按内容计算列宽*
): number {
  // 先判断列宽缓存里的列宽,再判断是否配置中针对该列定义了列宽
  let width = getColWidthDefinedWidthResizedWidth(col, table);
  
  if (
    table.internalProps.transpose &&
    width === 'auto' &&
    ((table.columnWidthComputeMode === 'only-header' && col >= table.rowHeaderLevelCount) ||
      (table.columnWidthComputeMode === 'only-body' && col < table.rowHeaderLevelCount))
  ) {
    width = table.getDefaultColumnWidth(col);
  }

  if (forceCompute && !table.internalProps.transpose) {
    return computeAutoColWidth(width, col, startRow, endRow, forceCompute, table);
  } else if (typeof width === 'number') {
    return width;
  } else if (width !== 'auto' && typeof width === 'string') {
    *// return calc.toPx(width, table.internalProps.calcWidthContext);*
    return table._adjustColWidth(col, table._colWidthDefineToPxWidth(width));
  }
  return computeAutoColWidth(width, col, startRow, endRow, forceCompute, table);
}    

Flowchart

Auto Calculate Column Width

In the previous process, there will be logic involving automatic calculation of column width. The core logic for calculating column width is located in computeAutoColWidth.

  • computeAutoColWidth(packages\vtable\src\scenegraph\layout\compute-col-width.ts)

Single Text Width Measurement

In the previous process of calculating width, there will be situations involving measuring text width. Let's analyze the process of measuring single text width below. \r

Overall Process
  • computeTextWidth (packages\vtable\src\scenegraph\layout\compute-col-width.ts)
Merging Cells Handling

For merged cells, a text will be divided by multiple cells, so after calculating the width, it needs to be divided by the number of columns spanned by the merged cells to calculate the actual width occupied by the current cell. \r

Calculation Formulas for Different Types of Cell Widths

After calculating the basic cell width, certain special cells need to be readjusted. Take the radio button as an example: \r

  • Radio button calculation formula: \r

Column Width Calculation Overall Process

Recalculate

Trigger Timing

There are multiple trigger points for recalculation, including: \r

  • Expand/Collapse Header

  • Change cell value

  • Add rows and columns

  • Click to sort

Source Code & Implementation

Let's take recalculateColWidths, which is triggered when a new row is added, as an example to explain the process of recalculating column widths:

// packages\vtable\src\scenegraph\scenegraph.ts
  */**
*   * recalculates column width in all autowidth columns*
*   */*
  recalculateColWidths() {
    const table = this.table;

    if (table.widthMode === 'adaptive' || table.autoFillWidth || table.internalProps.transpose) {
      computeColsWidth(this.table, 0, this.table.colCount - 1, true);
    } else {
      table._clearColRangeWidthsMap();
      *// left frozen*
      if (table.frozenColCount > 0) {
        computeColsWidth(this.table, 0, table.frozenColCount - 1, true);
      }
      *// right frozen*
      if (table.rightFrozenColCount > 0) {
        computeColsWidth(this.table, table.rightFrozenColCount, table.colCount - 1, true);
      }
      *// body*
      computeColsWidth(table, this.proxy.colStart, this.proxy.colEnd, true);
    }
  }    

It can be seen that VTable gradually updates all the columns, where the fourth parameter of all computeColsWidth is true. Let's see what VTable does when update is true.\r

  • Source code
// packages\vtable\src\scenegraph\layout\compute-col-width.ts
function computeColsWidth() {
// ...
  if (update) {
    for (let col = 0; col < table.colCount; col++) {
      const newColWidth = newWidths[col] ?? table.getColWidth(col) ?? table.getColWidth(col);
      if (newColWidth !== oldColWidths[col]) {
        table._setColWidth(col, newColWidth, false, true);
      }
    }
    table.stateManager.checkFrozen();
    for (let col = 0; col < table.colCount; col++) {
      const newColWidth = table.getColWidth(col);
      if (newColWidth !== oldColWidths[col]) {
        table.scenegraph.updateColWidth(col, newColWidth - oldColWidths[col], true, true);
      }
    }
    table.scenegraph.updateContainer(true);
  }
  //...
 }    

It can be seen that the internal process will judge column by column, comparing the newly calculated width with the old width. Only when the width changes will it readjust the table width and update the scene tree elements. Then update the scene tree container. \r

Line Height Calculation

Next, let's look at the logic for calculating line height. \r

Altitude Calculation Mode

There are three modes for calculating line height: 'standard' (standard mode), 'adaptive' (adaptive container height mode), or 'autoHeight' (automatic line height mode), with 'standard' as the default.

  • 'standard': Use defaultRowHeight and defaultHeaderRowHeight as row height;

  • 'adaptive': Scale proportionally based on the calculated height and the ratio of the container height to the calculated height;

  • 'autoHeight': Automatically calculate row height based on content, calculated based on fontSize and lineHeight (text line height), as well as padding. Related setting option autoWrapText for automatic line wrapping can calculate row height based on the content of the wrapped multi-line text;
Note that when autoFillHeight = true is configured, the enlargement according to the ratio will only occur if the row height does not exceed the container height.
### Core Processing

Overall Process

  • computeRowsHeight

The logic calculated for each line individually is mainly located in computeRowHeight, which calculates the row height based on the configuration information.

Pre-Update Check

To enter automatic line height calculation, one of the following conditions must be met:

body section update

Regarding the update of the body section, for certain special cases, there will be some performance optimizations. Let's see how it is specifically operated:

  • Display in columns
// packages\vtable\src\scenegraph\layout\compute-row-height.ts
if (
  *// 以列展示 且符合只需要计算第一行其他行可复用行高的条条件*
  !(
    table.internalProps.transpose ||
    (table.isPivotTable() && !(table.internalProps.layoutMap as PivotHeaderLayoutMap).indicatorsAsCol)
  ) &&
  !(table.options as ListTableConstructorOptions).customComputeRowHeight &&
  checkFixedStyleAndNoWrap(table)
) {
  *// check fixed style and no wrap situation, fill all row width single compute*
  *// traspose table and row indicator pivot table cannot use single row height*
  const height = computeRowHeight(table.columnHeaderLevelCount, 0, table.colCount - 1, table);
  fillRowsHeight(
    height,
    table.columnHeaderLevelCount,
    table.rowCount - 1 - table.bottomFrozenRowCount,
    table,
    update ? newHeights : undefined
  );
  *//底部冻结的行行高需要单独计算*
  for (let row = table.rowCount - table.bottomFrozenRowCount; row <= rowEnd; row++) {
    const height = computeRowHeight(row, 0, table.colCount - 1, table);
    if (update) {
      newHeights[row] = Math.round(height);
    } else {
      table._setRowHeight(row, height);
    }
  }
}    

  • Precondition check

  • Table row and column transposition is not enabled or it is not a pivot table \r

  • No custom line height calculation configured \r

  • checkFixedStyleAndNoWrap table columns and cell styles can be reused

  • Specific logic

  • Only calculate the first line in the body, reuse that height for other lines \r

  • The row height of the frozen bottom row needs to be calculated separately \r

  • Display by line

// packages\vtable\src\scenegraph\layout\compute-row-height.ts
if (
  *// 以行展示*
  table.internalProps.transpose ||
  (table.isPivotTable() && !(table.internalProps.layoutMap as PivotHeaderLayoutMap).indicatorsAsCol)
) {
  for (let row = Math.max(rowStart, table.columnHeaderLevelCount); row <= rowEnd; row++) {
    let height;
    if (checkFixedStyleAndNoWrapForTranspose(table, row)) {
      *// 以行展示 只计算到body第一列样式的情况即可*
      height = computeRowHeight(row, 0, table.rowHeaderLevelCount, table);
    } else {
      height = computeRowHeight(row, 0, table.colCount - 1, table);
    }
    if (update) {
      newHeights[row] = Math.round(height);
    } else {
      table._setRowHeight(row, height);
    }
  }
}    

  • Precondition check

  • The table is a transposed table or a pivot table with indicatorsAsCol configured as false \r

  • Specific logic

  • Loop through the body row section

  • Styles are reusable, and the line height calculation range only involves the line header

  • Non-reusable, line height calculation range to the end of the column

  • Fallback, loop through the body section, call computeRowHeight line by line \r

// packages\vtable\src\scenegraph\layout\compute-row-height.ts
*// 以列展示 需要逐行计算情况*
for (let row = Math.max(rowStart, table.columnHeaderLevelCount); row <= rowEnd; row++) {
  const height = computeRowHeight(row, 0, table.colCount - 1, table);
  if (update) {
    newHeights[row] = Math.round(height);
  } else {
    table._setRowHeight(row, height);
  }
}    

High Reusability Determination

In cases where autoWrapText or enableLineBreak is set to true, the line height cannot be reused, and calculations need to be done for each line.
* Judgment of ordinary cells `checkFixedStyleAndNoWrap`
// packages\vtable\src\scenegraph\layout\compute-row-height.ts
function checkFixedStyleAndNoWrap(table: BaseTableAPI): boolean {
  const { layoutMap } = table.internalProps;
  const row = table.columnHeaderLevelCount;
  *//设置了全局自动换行的话 不能复用高度计算*
  if (
    (table.internalProps.autoWrapText || table.internalProps.enableLineBreak || table.isPivotChart()) &&
    (table.isAutoRowHeight() || table.options.heightMode === 'adaptive')
  ) {
    return false;
  }
  // 每列都需要判断
  for (let col = 0; col < table.colCount; col++) {
    const cellDefine = layoutMap.getBody(col, row);
    if (cellDefine.cellType === 'radio') {
      return false;
    }
    // 判断是否配置了自定义函数
    if (
      typeof cellDefine.style === 'function' ||
      typeof (cellDefine as ColumnData).icon === 'function' ||
      (cellDefine.define as ColumnDefine)?.customRender ||
      (cellDefine.define as ColumnDefine)?.customLayout ||
      typeof cellDefine.define?.icon === 'function'
    ) {
      return false;
    }
    const cellStyle = table._getCellStyle(col, row); *//获取的style是结合了theme配置的style*
    if (
      typeof cellStyle.padding === 'function' ||
      typeof cellStyle.fontSize === 'function' ||
      typeof cellStyle.lineHeight === 'function' ||
      cellStyle.autoWrapText === true
    ) {
      return false;
    }
  }
    

  • Determine checkFixedStyleAndNoWrapForTranspose in the case of transposed tables
// packages\vtable\src\scenegraph\layout\compute-row-height.ts
function checkFixedStyleAndNoWrapForTranspose(table: BaseTableAPI, row: number): boolean {
  const { layoutMap } = table.internalProps;
  *//设置了全局自动换行的话 不能复用高度计算*
  if (
    (table.internalProps.autoWrapText || table.internalProps.enableLineBreak) &&
    (table.isAutoRowHeight() || table.options.heightMode === 'adaptive')
  ) {
    return false;
  }

  const cellDefine = layoutMap.getBody(table.rowHeaderLevelCount, row);
  // 判断是否配置了自定义函数
  if (
    typeof cellDefine.style === 'function' ||
    typeof (cellDefine as ColumnData).icon === 'function' ||
    (cellDefine.define as ColumnDefine)?.customRender ||
    (cellDefine.define as ColumnDefine)?.customLayout ||
    typeof cellDefine.define?.icon === 'function'
  ) {
    return false;
  }
  const cellStyle = table._getCellStyle(table.rowHeaderLevelCount, row);
  if (
    typeof cellStyle.padding === 'function' ||
    typeof cellStyle.fontSize === 'function' ||
    typeof cellStyle.lineHeight === 'function' ||
    cellStyle.autoWrapText === true
  ) {
    return false;
  }

  return true;
}    

Single Line Height Calculation

  • computeRowHeight

Text Height Calculation

autoWrapText mainly affects the calculation of text height. In the case of automatic line wrapping, AABBBounds will be generated, and the width of the text will be passed in during generation. This allows the height of the text to be calculated directly through AABBBounds. When automatic line wrapping is not enabled, only lineHeight will be used as the text height.

  • computeTextHeight process

The overall calculation formula is \u0060(Math.max(maxHeight, iconHeight) \u002B padding[0] \u002B padding[2]) / spanRow;\u0060 \r

General Process

Update Again

Taking the resize of the scene tree as an example, the row height will only be recalculated when heightMode is adaptive or autoFillHeight is true. There are several situations here: \r

// packages\vtable\src\scenegraph\scenegraph.ts
resize() {
    // ...
    if (this.table.heightMode === 'adaptive') {
      if (this.table.internalProps._heightResizedRowMap.size === 0) {
        this.recalculateRowHeights();
      } else {
        this.dealHeightMode();
      }
    } else if (this.table.autoFillHeight) {
      this.dealHeightMode();
    }
}    

  • When the column width is not adjusted, it is necessary to recalculate the row height. During the calculation, the scene tree elements will be updated based on the changed rows. \r
  • recalculateRowHeights ()
// packages\vtable\src\scenegraph\scenegraph.ts
  recalculateRowHeights() {
    const table = this.table;
    table.internalProps.useOneRowHeightFillAll = false;
    if (table.heightMode === 'adaptive' || table.autoFillHeight) {
      computeRowsHeight(this.table, 0, this.table.rowCount - 1, true, true);
    } else {
      *// top frozen*
      if (table.frozenRowCount > 0) {
        computeRowsHeight(this.table, 0, table.frozenRowCount - 1, true, true);
      }
      *// bottom frozen*
      if (table.bottomFrozenRowCount > 0) {
        computeRowsHeight(this.table, table.bottomFrozenRowCount, table.rowCount - 1, true, true);
      }
      computeRowsHeight(table, this.proxy.rowStart, this.proxy.rowEnd, true, true);
    }
  }

    

  • If the column width has been manually adjusted or autoFillHeight is enabled, it will enter dealHeightMode. \r

Because there is no need to calculate the line height in standard mode, the cache can be used directly during resize. Therefore, only in adaptive or autoFillHeight mode will the height of each line be readjusted and allocated based on the cached height.

  • dealHeightMode
// packages\vtable\src\scenegraph\scenegraph.ts
  dealHeightMode() {
    const table = this.table;
    *// 处理adaptive高度*
    if (table.heightMode === 'adaptive') {
      *// 清空行高缓存*
      table._clearRowRangeHeightsMap();
      *// 计算可分配高度 = 总高度 - 表头高度 - 底部冻结高度*
      const totalDrawHeight = table.tableNoFrameHeight - columnHeaderHeight - bottomHeaderHeight;
      *// 计算实际内容高度*
      for (let row = startRow; row < endRow; row++) {
        actualHeight += table.getRowHeight(row);
      }
      *// 计算缩放比例*
      const factor = totalDrawHeight / actualHeight;
      *// 按比例分配行高(最后一行处理余数)*
      for (let row = startRow; row < endRow; row++) {
        if (row === endRow - 1) {
          rowHeight = totalDrawHeight - 前N-1行总高度;
        } else {
          rowHeight = Math.round(原始行高 * factor);
        }
      }
    } else if (table.autoFillHeight) {
      *// 计算总内容高度*
      for (let row = 0; row < table.rowCount; row++) {
        actualHeight += table.getRowHeight(row);
      }
      *// 当实际高度 < 画布高度时*
      if (实际高度 < 画布高度) {
        *// 计算缩放比例(排除表头)*
        const factor = (canvasHeight - 表头高度) / (实际高度 - 表头高度);
        *// 按比例对行高进行缩放(最后一行处理剩余可分配高度)*
        for (let row = startRow; row < endRow; row++) {
          if (row === endRow - 1) {
            rowHeight = 剩余可分配高度;
          } else {
            rowHeight = Math.round(原始行高 * factor);
          }
        }
      }
    }
  }    

This document is provided by the following personnel

taiiiyang( https://github.com/taiiiyang)

This document was revised and organized by the following personnel

玄魂