Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: 使用离屏 Canvas 测量文本, 提高宽度计算的准确性 close #3018 #3053

Open
wants to merge 2 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2271,12 +2271,12 @@ Array [
"width": 113.6,
},
Object {
"actualText": "2367236723611...",
"actualText": "236723672361111",
"actualTextHeight": 15,
"actualTextWidth": 96,
"actualTextWidth": 98,
"height": 30,
"multiLineActualTexts": Array [
"2367236723611...",
"236723672361111",
],
"originalText": 236723672361111,
"width": 113.6,
Expand Down Expand Up @@ -6356,8 +6356,8 @@ Array [
"actualTextWidth": 100,
"height": 30,
"multiLineActualTexts": Array [
"236723672361",
"111",
"2367236723611",
"11",
],
"originalText": 236723672361111,
"width": 102.57,
Expand Down Expand Up @@ -12254,8 +12254,8 @@ Array [
"actualTextWidth": 100,
"height": 30,
"multiLineActualTexts": Array [
"236723672361",
"111",
"2367236723611",
"11",
],
"originalText": 236723672361111,
"width": 102.57,
Expand Down Expand Up @@ -15261,8 +15261,8 @@ Array [
"actualTextWidth": 100,
"height": 30,
"multiLineActualTexts": Array [
"236723672361",
"111",
"2367236723611",
"11",
],
"originalText": 236723672361111,
"width": 102.57,
Expand Down
193 changes: 183 additions & 10 deletions packages/s2-core/__tests__/spreadsheet/compare-layout-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const s2Options: S2Options = {

describe('Compare Layout Tests', () => {
const expectTextOverflowing = (s2: SpreadSheet) => {
[...s2.facet.getColCells(), ...s2.facet.getDataCells()].forEach((cell) => {
s2.facet.getCells().forEach((cell) => {
expect(cell.getTextShape().isOverflowing()).toBeFalsy();
});
};
Expand Down Expand Up @@ -55,9 +55,9 @@ describe('Compare Layout Tests', () => {

const colLeafNodes = s2.facet.getColLeafNodes();

expect(Math.floor(colLeafNodes[0].width)).toBeCloseTo(133);
expect(Math.floor(colLeafNodes[0].width)).toBeCloseTo(122);
expect(Math.floor(colLeafNodes[1].width)).toEqual(
options.showDefaultHeaderActionIcon ? 71 : 66,
options.showDefaultHeaderActionIcon ? 66 : 63,
);
expectTextOverflowing(s2);
},
Expand All @@ -84,15 +84,15 @@ describe('Compare Layout Tests', () => {
});
await s2.render();

const expectWidth = options.showDefaultHeaderActionIcon ? 71 : 66;
const expectWidth = options.showDefaultHeaderActionIcon ? 66 : 63;
const isLargeFontSize = options.fontSize === 20;
const colLeafNodes = s2.facet.getColLeafNodes();

expect(Math.floor(colLeafNodes[0].width)).toBeCloseTo(
isLargeFontSize ? 209 : 133,
isLargeFontSize ? 191 : 122,
);
expect(Math.floor(colLeafNodes[1].width)).toEqual(
isLargeFontSize ? 97 : expectWidth,
isLargeFontSize ? 92 : expectWidth,
);
expectTextOverflowing(s2);
});
Expand Down Expand Up @@ -123,7 +123,7 @@ describe('Compare Layout Tests', () => {

const colLeafNodes = s2.facet.getColLeafNodes();
const { dataCellWidthList, colLeafNodeWidthList } = mapWidthList(s2);
const expectWidth = 207;
const expectWidth = 183;

expect(Math.floor(colLeafNodes[0].width)).toBeCloseTo(expectWidth);
expect(
Expand Down Expand Up @@ -171,13 +171,186 @@ describe('Compare Layout Tests', () => {

expect(dataCellWidthList).toEqual(
options.showDefaultHeaderActionIcon
? [227, 227, 227, 227, 115, 115, 115, 115, 93, 93, 93, 93]
: [227, 227, 227, 227, 115, 115, 115, 115, 71, 71, 71, 71],
? [209, 209, 209, 209, 110, 110, 110, 110, 85, 85, 85, 85]
: [209, 209, 209, 209, 110, 110, 110, 110, 69, 69, 69, 69],
);
expect(colLeafNodeWidthList).toEqual(
options.showDefaultHeaderActionIcon ? [227, 115, 93] : [227, 115, 71],
options.showDefaultHeaderActionIcon ? [209, 110, 85] : [209, 110, 69],
);
expectTextOverflowing(s2);
},
);

test.each([{ showIcon: true }, { showIcon: false }])(
'should get max row width for pivot sheet and format name by %o',
async (options) => {
const s2 = new PivotSheet(
getContainer(),
{
...mockDataConfig,
meta: [
{ field: 'province', name: '省份666' },
{ field: 'city', name: '城市1234567' },
],
},
{
...s2Options,
headerActionIcons: options.showIcon
? [
{
icons: ['SortDown'],
belongsCell: 'cornerCell',
},
]
: [],
},
);

await s2.render();

const rowNodes = s2.facet.getRowNodes();

expect(Math.floor(rowNodes[0].width)).toBeCloseTo(
options.showIcon ? 80 : 62,
);
expect(Math.floor(rowNodes[1].width)).toEqual(
options.showIcon ? 106 : 88,
);
expectTextOverflowing(s2);
},
);

test('should not render overflowing text for table sheet and a difference type text', async () => {
const s2 = new TableSheet(getContainer(), mockDataConfig, s2Options);

s2.setDataCfg({
fields: {
columns: [
'date',
'zh',
'percentage',
'number',
'url-number',
'url-en',
'url-zh',
],
},
meta: [
{
field: 'date',
formatter: () => '2021-09-08',
},
{
field: 'zh',
formatter: () => '中文文本测试中文文本',
},
{
field: 'percentage',
formatter: () => '100.23433333%',
},
{
field: 'number',
formatter: () => '111111111111',
},
{
field: 'url-number',
formatter: () => `https://wwww.test.cn?test=${'1'.repeat(10)}`,
},
{
field: 'url-en',
formatter: () => `https://wwww.test.cn?test=${'t'.repeat(10)}`,
},
{
field: 'url-zh',
formatter: () => `https://wwww.test.cn?test=${'测'.repeat(10)}`,
},
],
});

await s2.render();

expectTextOverflowing(s2);
});

test('should get max col width for pivot sheet conditions', async () => {
const s2 = new PivotSheet(getContainer(), mockDataConfig, {
...s2Options,
conditions: {
icon: [
{
field: 'price',
position: 'left',
mapping(fieldValue: number) {
if (!fieldValue) {
return null;
}

return fieldValue > 0
? {
fill: 'red',
icon: 'CellUp',
}
: {
fill: 'green',
icon: 'CellDown',
};
},
},
],
},
});

s2.setDataCfg({
meta: [
{
field: 'price',
formatter: (value) => (value === '111' ? '35333.7%' : value),
},
],
});

await s2.render();

const { dataCellWidthList, colLeafNodeWidthList } = mapWidthList(s2);

expect(dataCellWidthList).toEqual([
140, 140, 140, 140, 81, 81, 81, 81, 92, 92, 92, 92,
]);
expect(colLeafNodeWidthList).toEqual([140, 81, 92]);
expectTextOverflowing(s2);
});

test.each([
{ text: '中文文本测试中文文本测试', width: 145 },
{ text: '中文文本测试中文文本123', width: 142 },
{ text: '中文文本测试中文文本word', width: 150 },
{ text: '11111111111111111', width: 104 },
{ text: '参数:', width: 37 },
{ text: 'word', width: 30 },
{ text: 'word123', width: 50 },
{ text: 'word123...', width: 60 },
{ text: '100.234%', width: 56 },
{ text: '2024-12-24', width: 63 },
{ text: '纸张123456', width: 66 },
{ text: `https://wwww.test.cn?test=${'1'.repeat(10)}`, width: 217 },
])('should get correctly text width for %o', async ({ text, width }) => {
const s2 = new PivotSheet(getContainer(), mockDataConfig, {
...s2Options,
});

await s2.render();

const result = s2.facet.measureTextWidth(text, {
fontFamily: 'Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif',
fontSize: 12,
fontWeight: 700,
fill: '#000000',
opacity: 1,
textAlign: 'left',
textBaseline: 'middle',
linkTextFill: '#2C60D4',
});

expect(result).toEqual(width);
});
});
2 changes: 1 addition & 1 deletion packages/s2-core/__tests__/spreadsheet/scroll-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ describe('Scroll Tests', () => {
s2.changeSheetSize(1000, 150); // 纵向滚动条
await s2.render(false);

expect(Math.floor(s2.facet.vScrollBar.getBBox().x)).toEqual(213);
expect(Math.floor(s2.facet.vScrollBar.getBBox().x)).toEqual(201);

s2.setOptions({
interaction: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ describe('Col width Test', () => {
const colLeafNodes = s2.facet.getColLeafNodes();

// price 列,列头标签比表身数据更长
expect(Math.round(colLeafNodes[0].width)).toBe(52);
expect(Math.round(colLeafNodes[0].width)).toBe(47);
// cost 列,表身数据比列头更长(格式化)
expect(Math.round(colLeafNodes[1].width)).toBe(168);
});
Expand Down
2 changes: 1 addition & 1 deletion packages/s2-core/__tests__/unit/utils/canvas-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getOffscreenCanvas, removeOffscreenCanvas } from '@/utils';
import { sleep } from './../../util/helpers';

describe('Canvas Utils Tests', () => {
const ID = 's2-offscreen-canvas';
const ID = 'antv-s2-offscreen-canvas';

test('should get offscreen canvas', () => {
const canvas = getOffscreenCanvas();
Expand Down
6 changes: 3 additions & 3 deletions packages/s2-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@
},
"dependencies": {
"@antv/event-emitter": "^0.1.3",
"@antv/g": "^6.1.15",
"@antv/g-canvas": "^2.0.33",
"@antv/g-lite": "^2.2.10",
"@antv/g": "^6.1.17",
"@antv/g-canvas": "^2.0.35",
"@antv/g-lite": "^2.2.12",
"d3-ease": "^3.0.1",
"d3-interpolate": "^1.3.2",
"d3-timer": "^1.0.9",
Expand Down
2 changes: 1 addition & 1 deletion packages/s2-core/scripts/test-live.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import ora from 'ora';
inquirer.registerPrompt('autocomplete', autoCompletePrompt);

function run(path) {
const command = `cross-env DEBUG_MODE=1 npx jest ${path} --passWithNoTests --detectOpenHandles`;
const command = `cross-env DEBUG_MODE=1 npx jest ${path} --passWithNoTests`;
const jestSpinner = ora(`[测试运行中]: ${command}`).start();

try {
Expand Down
3 changes: 2 additions & 1 deletion packages/s2-core/src/cell/base-cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,13 +491,14 @@ export abstract class BaseCell<T extends SimpleBBox> extends Group {
const maxTextWidth = Math.max(this.getMaxTextWidth(), 0) + EXTRA_PIXEL;
const textStyle = this.getTextStyle();
const maxLines = this.getResizedTextMaxLines() || textStyle?.maxLines;
const text = this.getFieldValue()!;

// 在坐标计算 (getTextPosition) 之前, 预渲染一次, 提前生成 textShape, 获得文字宽度, 用于计算 icon 绘制坐标
this.renderTextShape({
...textStyle,
x: 0,
y: 0,
text: this.getFieldValue()!,
text,
wordWrapWidth: maxTextWidth,
maxLines,
});
Expand Down
14 changes: 1 addition & 13 deletions packages/s2-core/src/facet/base-facet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2454,22 +2454,10 @@ export abstract class BaseFacet {
* @tip 和 this.spreadsheet.measureTextWidth() 的区别在于:
* 1. 额外添加一像素余量,防止 maxLabel 有多个同样长度情况下,一些 label 不能展示完全, 出现省略号
* 2. 测量时, 文本宽度取整, 避免子像素的不一致性
* 3. TODO: 由于 G 测量文本是一个一个字符进行计算, 在数字/英文等场景会有较大误差, 这里为了防止紧凑模式出现省略号, 暂时保持一样的策略
*/
protected measureTextWidth(
text: SimpleData,
font: unknown,
roughly = true,
): number {
protected measureTextWidth(text: SimpleData, font: unknown): number {
const EXTRA_PIXEL = 1;

if (roughly) {
return (
Math.ceil(this.spreadsheet.measureTextWidthRoughly(text, font)) +
EXTRA_PIXEL
);
}

return (
Math.ceil(this.spreadsheet.measureTextWidth(text, font)) + EXTRA_PIXEL
);
Expand Down
2 changes: 1 addition & 1 deletion packages/s2-core/src/facet/pivot-facet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ export class PivotFacet extends FrozenFacet {
* 额外增加 1,当内容和容器宽度恰好相等时会出现换行
*/
const maxLabelWidth =
this.measureTextWidth(treeHeaderLabel, cornerCellTextStyle, false) +
this.measureTextWidth(treeHeaderLabel, cornerCellTextStyle) +
cornerIconStyle.size * 2 +
cornerIconStyle.margin?.left +
cornerIconStyle.margin?.right +
Expand Down
2 changes: 1 addition & 1 deletion packages/s2-core/src/facet/table-facet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class TableFacet extends FrozenFacet {
const iconX = viewportWidth / 2 - icon.width / 2;
const iconY = height / 2 + maxY - icon.height / 2 + icon.margin.top;
const text = empty?.description ?? i18n('暂无数据');
const descWidth = this.measureTextWidth(text, description, false);
const descWidth = this.measureTextWidth(text, description);
const descX = viewportWidth / 2 - descWidth / 2;
const descY = iconY + icon.height + icon.margin.bottom;

Expand Down
Loading
Loading