From 0ade7949e63d3e14e2e248ec533134b62ed0c4b4 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 12 Aug 2026 15:59:35 +0800 Subject: [PATCH 1/9] fix(vtable): defer chart constructor render retry Allow pivot chart construction to complete when initial embedded chart rendering depends on callbacks that need the assigned table instance. Co-Authored-By: Claude Sonnet 4.6 --- .../vtable/__tests__/chart-graphic.test.ts | 66 +++++++++++++++++++ .../vtable/src/scenegraph/graphic/chart.ts | 25 ++++++- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/packages/vtable/__tests__/chart-graphic.test.ts b/packages/vtable/__tests__/chart-graphic.test.ts index 0e0d859437..c6bd961cdf 100644 --- a/packages/vtable/__tests__/chart-graphic.test.ts +++ b/packages/vtable/__tests__/chart-graphic.test.ts @@ -24,7 +24,32 @@ class MockChart { } } +class ThrowOnceChart extends MockChart { + renderCount = 0; + dirtyBoundsCount = 0; + + renderSync() { + this.renderCount++; + if (this.renderCount === 1) { + throw new Error('render before table instance is assigned'); + } + } + + getStage() { + return { + enableDirtyBounds: () => { + this.dirtyBoundsCount++; + } + }; + } +} + describe('Chart graphic', () => { + afterEach(() => { + jest.useRealTimers(); + jest.restoreAllMocks(); + }); + test('keeps runtime refs when VRender builds static state snapshots', () => { const canvas = document.createElement('canvas') as HTMLCanvasElement & { __vtable__?: unknown }; const tableRef: { internalProps?: unknown } = {}; @@ -62,4 +87,45 @@ describe('Chart graphic', () => { expect(chart.attribute.chartInstance).toBe(chart.chartInstance); expect(chart.attribute.chartInstance).toBe((chart as any).baseAttributes.chartInstance); }); + + test('defers constructor render errors so chart instances can be assigned before retrying', () => { + jest.useFakeTimers(); + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => undefined); + const canvas = document.createElement('canvas'); + + let chart: Chart | undefined; + expect(() => { + chart = new Chart(false, { + stroke: false, + x: 0, + y: 0, + width: 100, + height: 80, + canvas, + mode: 'desktop-browser', + modeParams: {}, + spec: { type: 'bar' }, + ClassType: ThrowOnceChart, + chartInstance: undefined, + dataId: 'data', + data: [], + cellPadding: [0, 0, 0, 0], + dpr: 1, + axes: [], + tableChartOption: {}, + detectPickChartItem: false + } as any); + }).not.toThrow(); + + const chartInstance = chart?.chartInstance as ThrowOnceChart; + expect(chartInstance).toBeInstanceOf(ThrowOnceChart); + expect(chartInstance.renderCount).toBe(1); + expect(chartInstance.dirtyBoundsCount).toBe(0); + + jest.runOnlyPendingTimers(); + + expect(chartInstance.renderCount).toBe(2); + expect(chartInstance.dirtyBoundsCount).toBe(1); + expect(consoleError).not.toHaveBeenCalled(); + }); }); diff --git a/packages/vtable/src/scenegraph/graphic/chart.ts b/packages/vtable/src/scenegraph/graphic/chart.ts index 651b1a5f97..97e837814b 100644 --- a/packages/vtable/src/scenegraph/graphic/chart.ts +++ b/packages/vtable/src/scenegraph/graphic/chart.ts @@ -61,6 +61,28 @@ const CHART_RUNTIME_ATTRIBUTE_KEYS: (keyof IChartGraphicAttribute)[] = [ export const CHART_NUMBER_TYPE = genNumberType(); +function renderChartInstanceInConstructor(chartInstance: any) { + try { + chartInstance.renderSync(); + chartInstance.getStage().enableDirtyBounds(); + } catch (error) { + if (typeof setTimeout !== 'function') { + throw error; + } + + // Some chart callbacks need the table instance that is assigned only after + // `new PivotChart(...)` returns. Retry once after construction completes. + setTimeout(() => { + try { + chartInstance.renderSync(); + chartInstance.getStage().enableDirtyBounds(); + } catch (retryError) { + console.error(retryError); + } + }, 0); + } +} + export class Chart extends Rect { type: GraphicType = 'chart' as any; declare attribute: IChartGraphicAttribute; @@ -102,8 +124,7 @@ export class Chart extends Rect { autoFit: false }) )); - chartInstance.renderSync(); - chartInstance.getStage().enableDirtyBounds(); + renderChartInstanceInConstructor(chartInstance); params.chartInstance = this.chartInstance = chartInstance; this.syncRuntimeAttributes({ chartInstance } as Partial); } else { From 25aa78e9de8b6a7355f24fe7b790d5928d2ab2ef Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 13 Aug 2026 11:18:41 +0800 Subject: [PATCH 2/9] fix(vtable): preserve chart retry errors Let delayed chart constructor retries surface persistent render failures instead of downgrading them to console logs. Co-Authored-By: Claude Sonnet 4.6 --- .../vtable/__tests__/chart-graphic.test.ts | 47 +++++++++++++++++-- .../vtable/src/scenegraph/graphic/chart.ts | 8 +--- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/vtable/__tests__/chart-graphic.test.ts b/packages/vtable/__tests__/chart-graphic.test.ts index c6bd961cdf..03fdeffe2c 100644 --- a/packages/vtable/__tests__/chart-graphic.test.ts +++ b/packages/vtable/__tests__/chart-graphic.test.ts @@ -44,10 +44,16 @@ class ThrowOnceChart extends MockChart { } } +class AlwaysThrowChart extends ThrowOnceChart { + renderSync() { + this.renderCount++; + throw new Error('persistent render failure'); + } +} + describe('Chart graphic', () => { afterEach(() => { jest.useRealTimers(); - jest.restoreAllMocks(); }); test('keeps runtime refs when VRender builds static state snapshots', () => { @@ -90,7 +96,6 @@ describe('Chart graphic', () => { test('defers constructor render errors so chart instances can be assigned before retrying', () => { jest.useFakeTimers(); - const consoleError = jest.spyOn(console, 'error').mockImplementation(() => undefined); const canvas = document.createElement('canvas'); let chart: Chart | undefined; @@ -126,6 +131,42 @@ describe('Chart graphic', () => { expect(chartInstance.renderCount).toBe(2); expect(chartInstance.dirtyBoundsCount).toBe(1); - expect(consoleError).not.toHaveBeenCalled(); + }); + + test('throws persistent constructor render errors on retry', () => { + jest.useFakeTimers(); + const canvas = document.createElement('canvas'); + + let chart: Chart | undefined; + expect(() => { + chart = new Chart(false, { + stroke: false, + x: 0, + y: 0, + width: 100, + height: 80, + canvas, + mode: 'desktop-browser', + modeParams: {}, + spec: { type: 'bar' }, + ClassType: AlwaysThrowChart, + chartInstance: undefined, + dataId: 'data', + data: [], + cellPadding: [0, 0, 0, 0], + dpr: 1, + axes: [], + tableChartOption: {}, + detectPickChartItem: false + } as any); + }).not.toThrow(); + + const chartInstance = chart?.chartInstance as AlwaysThrowChart; + expect(chartInstance.renderCount).toBe(1); + + expect(() => { + jest.runOnlyPendingTimers(); + }).toThrow('persistent render failure'); + expect(chartInstance.renderCount).toBe(2); }); }); diff --git a/packages/vtable/src/scenegraph/graphic/chart.ts b/packages/vtable/src/scenegraph/graphic/chart.ts index 97e837814b..f3191f23e4 100644 --- a/packages/vtable/src/scenegraph/graphic/chart.ts +++ b/packages/vtable/src/scenegraph/graphic/chart.ts @@ -73,12 +73,8 @@ function renderChartInstanceInConstructor(chartInstance: any) { // Some chart callbacks need the table instance that is assigned only after // `new PivotChart(...)` returns. Retry once after construction completes. setTimeout(() => { - try { - chartInstance.renderSync(); - chartInstance.getStage().enableDirtyBounds(); - } catch (retryError) { - console.error(retryError); - } + chartInstance.renderSync(); + chartInstance.getStage().enableDirtyBounds(); }, 0); } } From 341135809548708dc2a17007beeb0d0cca5b86b7 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 13 Aug 2026 11:30:47 +0800 Subject: [PATCH 3/9] fix(vtable): narrow deferred chart render retry Only defer the known pending table-instance render error and keep other chart constructor failures synchronous. Co-Authored-By: Claude Sonnet 4.6 --- .../vtable/__tests__/chart-graphic.test.ts | 23 ++++++++----------- .../vtable/src/scenegraph/graphic/chart.ts | 18 +++++++++++++-- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/vtable/__tests__/chart-graphic.test.ts b/packages/vtable/__tests__/chart-graphic.test.ts index 03fdeffe2c..48c83b6e32 100644 --- a/packages/vtable/__tests__/chart-graphic.test.ts +++ b/packages/vtable/__tests__/chart-graphic.test.ts @@ -31,7 +31,9 @@ class ThrowOnceChart extends MockChart { renderSync() { this.renderCount++; if (this.renderCount === 1) { - throw new Error('render before table instance is assigned'); + throw new TypeError( + "Cannot destructure property 'col' of 'getCellAddressByRecord(...)' as it is undefined." + ); } } @@ -44,10 +46,10 @@ class ThrowOnceChart extends MockChart { } } -class AlwaysThrowChart extends ThrowOnceChart { +class InvalidSpecChart extends ThrowOnceChart { renderSync() { this.renderCount++; - throw new Error('persistent render failure'); + throw new Error('invalid chart spec'); } } @@ -133,7 +135,7 @@ describe('Chart graphic', () => { expect(chartInstance.dirtyBoundsCount).toBe(1); }); - test('throws persistent constructor render errors on retry', () => { + test('throws non-recoverable constructor render errors synchronously', () => { jest.useFakeTimers(); const canvas = document.createElement('canvas'); @@ -149,7 +151,7 @@ describe('Chart graphic', () => { mode: 'desktop-browser', modeParams: {}, spec: { type: 'bar' }, - ClassType: AlwaysThrowChart, + ClassType: InvalidSpecChart, chartInstance: undefined, dataId: 'data', data: [], @@ -159,14 +161,9 @@ describe('Chart graphic', () => { tableChartOption: {}, detectPickChartItem: false } as any); - }).not.toThrow(); - - const chartInstance = chart?.chartInstance as AlwaysThrowChart; - expect(chartInstance.renderCount).toBe(1); + }).toThrow('invalid chart spec'); - expect(() => { - jest.runOnlyPendingTimers(); - }).toThrow('persistent render failure'); - expect(chartInstance.renderCount).toBe(2); + expect(chart).toBeUndefined(); + expect(jest.getTimerCount()).toBe(0); }); }); diff --git a/packages/vtable/src/scenegraph/graphic/chart.ts b/packages/vtable/src/scenegraph/graphic/chart.ts index f3191f23e4..089713caac 100644 --- a/packages/vtable/src/scenegraph/graphic/chart.ts +++ b/packages/vtable/src/scenegraph/graphic/chart.ts @@ -61,17 +61,31 @@ const CHART_RUNTIME_ATTRIBUTE_KEYS: (keyof IChartGraphicAttribute)[] = [ export const CHART_NUMBER_TYPE = genNumberType(); +function isPendingTableInstanceRenderError(error: unknown) { + const errorLike = error as { name?: string; message?: string }; + return ( + errorLike?.name === 'TypeError' && + typeof errorLike.message === 'string' && + errorLike.message.includes('getCellAddressByRecord') && + errorLike.message.includes('undefined') + ); +} + function renderChartInstanceInConstructor(chartInstance: any) { try { chartInstance.renderSync(); chartInstance.getStage().enableDirtyBounds(); } catch (error) { + if (!isPendingTableInstanceRenderError(error)) { + throw error; + } + if (typeof setTimeout !== 'function') { throw error; } - // Some chart callbacks need the table instance that is assigned only after - // `new PivotChart(...)` returns. Retry once after construction completes. + // The Aeolus label callback may call getCellAddressByRecord before the + // external variable receives the newly constructed PivotChart instance. setTimeout(() => { chartInstance.renderSync(); chartInstance.getStage().enableDirtyBounds(); From 7cd2c143e3b4b9c692e59ebf29212356592c59a8 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 13 Aug 2026 12:43:12 +0800 Subject: [PATCH 4/9] fix(vtable): match deferred chart retry source Restrict delayed constructor retries to the Aeolus pipeline error that occurs before the external PivotChart instance is assigned. Co-Authored-By: Claude Sonnet 4.6 --- .../vtable/__tests__/chart-graphic.test.ts | 50 +++++++++++++++++-- .../vtable/src/scenegraph/graphic/chart.ts | 7 ++- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/packages/vtable/__tests__/chart-graphic.test.ts b/packages/vtable/__tests__/chart-graphic.test.ts index 48c83b6e32..eae164a3c8 100644 --- a/packages/vtable/__tests__/chart-graphic.test.ts +++ b/packages/vtable/__tests__/chart-graphic.test.ts @@ -1,5 +1,8 @@ import { Chart } from '../src/scenegraph/graphic/chart'; +const GET_CELL_ADDRESS_ERROR_MESSAGE = + "Cannot destructure property 'col' of 'getCellAddressByRecord(...)' as it is undefined."; + class MockChart { static globalConfig = { uniqueTooltip: false }; @@ -31,9 +34,11 @@ class ThrowOnceChart extends MockChart { renderSync() { this.renderCount++; if (this.renderCount === 1) { - throw new TypeError( - "Cannot destructure property 'col' of 'getCellAddressByRecord(...)' as it is undefined." - ); + const error = new TypeError(GET_CELL_ADDRESS_ERROR_MESSAGE); + error.stack = + "TypeError: Cannot destructure property 'col' of 'getCellAddressByRecord(...)' as it is undefined.\n" + + ' at https://sf-unpkg-src.bytedance.net/@aeolus/chart@0.0.11/dist/pipeline.js:32127:18'; + throw error; } } @@ -53,6 +58,13 @@ class InvalidSpecChart extends ThrowOnceChart { } } +class MissingCellAddressChart extends ThrowOnceChart { + renderSync() { + this.renderCount++; + throw new TypeError(GET_CELL_ADDRESS_ERROR_MESSAGE); + } +} + describe('Chart graphic', () => { afterEach(() => { jest.useRealTimers(); @@ -166,4 +178,36 @@ describe('Chart graphic', () => { expect(chart).toBeUndefined(); expect(jest.getTimerCount()).toBe(0); }); + + test('throws unmatched getCellAddressByRecord errors synchronously', () => { + jest.useFakeTimers(); + const canvas = document.createElement('canvas'); + + let chart: Chart | undefined; + expect(() => { + chart = new Chart(false, { + stroke: false, + x: 0, + y: 0, + width: 100, + height: 80, + canvas, + mode: 'desktop-browser', + modeParams: {}, + spec: { type: 'bar' }, + ClassType: MissingCellAddressChart, + chartInstance: undefined, + dataId: 'data', + data: [], + cellPadding: [0, 0, 0, 0], + dpr: 1, + axes: [], + tableChartOption: {}, + detectPickChartItem: false + } as any); + }).toThrow(GET_CELL_ADDRESS_ERROR_MESSAGE); + + expect(chart).toBeUndefined(); + expect(jest.getTimerCount()).toBe(0); + }); }); diff --git a/packages/vtable/src/scenegraph/graphic/chart.ts b/packages/vtable/src/scenegraph/graphic/chart.ts index 089713caac..aa249c860e 100644 --- a/packages/vtable/src/scenegraph/graphic/chart.ts +++ b/packages/vtable/src/scenegraph/graphic/chart.ts @@ -62,12 +62,15 @@ const CHART_RUNTIME_ATTRIBUTE_KEYS: (keyof IChartGraphicAttribute)[] = [ export const CHART_NUMBER_TYPE = genNumberType(); function isPendingTableInstanceRenderError(error: unknown) { - const errorLike = error as { name?: string; message?: string }; + const errorLike = error as { name?: string; message?: string; stack?: string }; return ( errorLike?.name === 'TypeError' && typeof errorLike.message === 'string' && + typeof errorLike.stack === 'string' && errorLike.message.includes('getCellAddressByRecord') && - errorLike.message.includes('undefined') + errorLike.message.includes('undefined') && + errorLike.stack.includes('@aeolus/chart') && + errorLike.stack.includes('pipeline') ); } From f3e73819f9b3ec88f84bcd53667d0ddf492ee08a Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 13 Aug 2026 14:15:56 +0800 Subject: [PATCH 5/9] fix(vtable): gate deferred chart render by construction state Use an explicit PivotChart construction signal for deferred label dataFilter rendering instead of matching error messages or stacks. Co-Authored-By: Claude Sonnet 4.6 --- .../vtable/__tests__/chart-graphic.test.ts | 9 ++--- packages/vtable/src/PivotChart.ts | 35 +++++++++++-------- .../vtable/src/scenegraph/graphic/chart.ts | 25 ++++--------- .../group-creater/cell-type/chart-cell.ts | 9 +++-- 4 files changed, 36 insertions(+), 42 deletions(-) diff --git a/packages/vtable/__tests__/chart-graphic.test.ts b/packages/vtable/__tests__/chart-graphic.test.ts index eae164a3c8..ae4204de30 100644 --- a/packages/vtable/__tests__/chart-graphic.test.ts +++ b/packages/vtable/__tests__/chart-graphic.test.ts @@ -34,11 +34,7 @@ class ThrowOnceChart extends MockChart { renderSync() { this.renderCount++; if (this.renderCount === 1) { - const error = new TypeError(GET_CELL_ADDRESS_ERROR_MESSAGE); - error.stack = - "TypeError: Cannot destructure property 'col' of 'getCellAddressByRecord(...)' as it is undefined.\n" + - ' at https://sf-unpkg-src.bytedance.net/@aeolus/chart@0.0.11/dist/pipeline.js:32127:18'; - throw error; + throw new TypeError(GET_CELL_ADDRESS_ERROR_MESSAGE); } } @@ -132,7 +128,8 @@ describe('Chart graphic', () => { dpr: 1, axes: [], tableChartOption: {}, - detectPickChartItem: false + detectPickChartItem: false, + deferRenderForTableConstructor: true } as any); }).not.toThrow(); diff --git a/packages/vtable/src/PivotChart.ts b/packages/vtable/src/PivotChart.ts index 0fdb4dc221..9792d7f8c9 100644 --- a/packages/vtable/src/PivotChart.ts +++ b/packages/vtable/src/PivotChart.ts @@ -268,23 +268,28 @@ export class PivotChart extends BaseTable implements PivotChartAPI { this.internalProps.useOneRowHeightFillAll = false; // this.internalProps.frozenColCount = this.options.frozenColCount || this.rowHeaderLevelCount; // 生成单元格场景树 - this.scenegraph.createSceneGraph(); - if (options.title) { - const Title = Factory.getComponent('title') as ITitleComponent; - this.internalProps.title = new Title(options.title, this); - // this.scenegraph.resize();//下面有个resize了 所以这个可以去掉 - } - if (this.options.emptyTip) { - if (this.internalProps.emptyTip) { - this.internalProps.emptyTip?.resetVisible(); - } else { - const EmptyTip = Factory.getComponent('emptyTip') as IEmptyTipComponent; - this.internalProps.emptyTip = new EmptyTip(this.options.emptyTip, this); - this.internalProps.emptyTip?.resetVisible(); + (this as any)._isConstructingPivotChart = true; + try { + this.scenegraph.createSceneGraph(); + if (options.title) { + const Title = Factory.getComponent('title') as ITitleComponent; + this.internalProps.title = new Title(options.title, this); + // this.scenegraph.resize();//下面有个resize了 所以这个可以去掉 } + if (this.options.emptyTip) { + if (this.internalProps.emptyTip) { + this.internalProps.emptyTip?.resetVisible(); + } else { + const EmptyTip = Factory.getComponent('emptyTip') as IEmptyTipComponent; + this.internalProps.emptyTip = new EmptyTip(this.options.emptyTip, this); + this.internalProps.emptyTip?.resetVisible(); + } + } + // 首次布局同样通过 BaseTable.resize() 完成,遵循 componentLayoutOrder 中的 title/legend 优先级 + this.resize(); + } finally { + (this as any)._isConstructingPivotChart = false; } - // 首次布局同样通过 BaseTable.resize() 完成,遵循 componentLayoutOrder 中的 title/legend 优先级 - this.resize(); //为了确保用户监听得到这个事件 这里做了异步 确保vtable实例已经初始化完成 setTimeout(() => { if (this.isReleased) { diff --git a/packages/vtable/src/scenegraph/graphic/chart.ts b/packages/vtable/src/scenegraph/graphic/chart.ts index aa249c860e..210a0384f8 100644 --- a/packages/vtable/src/scenegraph/graphic/chart.ts +++ b/packages/vtable/src/scenegraph/graphic/chart.ts @@ -43,6 +43,7 @@ interface IChartGraphicAttribute extends IGroupGraphicAttribute { col?: number; row?: number; detectPickChartItem?: boolean; + deferRenderForTableConstructor?: boolean; } const CHART_RUNTIME_ATTRIBUTE_KEYS: (keyof IChartGraphicAttribute)[] = [ @@ -56,30 +57,18 @@ const CHART_RUNTIME_ATTRIBUTE_KEYS: (keyof IChartGraphicAttribute)[] = [ 'cellPadding', 'axes', 'tableChartOption', - 'detectPickChartItem' + 'detectPickChartItem', + 'deferRenderForTableConstructor' ]; export const CHART_NUMBER_TYPE = genNumberType(); -function isPendingTableInstanceRenderError(error: unknown) { - const errorLike = error as { name?: string; message?: string; stack?: string }; - return ( - errorLike?.name === 'TypeError' && - typeof errorLike.message === 'string' && - typeof errorLike.stack === 'string' && - errorLike.message.includes('getCellAddressByRecord') && - errorLike.message.includes('undefined') && - errorLike.stack.includes('@aeolus/chart') && - errorLike.stack.includes('pipeline') - ); -} - -function renderChartInstanceInConstructor(chartInstance: any) { +function renderChartInstanceInConstructor(chartInstance: any, deferRenderForTableConstructor?: boolean) { try { chartInstance.renderSync(); chartInstance.getStage().enableDirtyBounds(); } catch (error) { - if (!isPendingTableInstanceRenderError(error)) { + if (!deferRenderForTableConstructor) { throw error; } @@ -87,8 +76,6 @@ function renderChartInstanceInConstructor(chartInstance: any) { throw error; } - // The Aeolus label callback may call getCellAddressByRecord before the - // external variable receives the newly constructed PivotChart instance. setTimeout(() => { chartInstance.renderSync(); chartInstance.getStage().enableDirtyBounds(); @@ -137,7 +124,7 @@ export class Chart extends Rect { autoFit: false }) )); - renderChartInstanceInConstructor(chartInstance); + renderChartInstanceInConstructor(chartInstance, params.deferRenderForTableConstructor); params.chartInstance = this.chartInstance = chartInstance; this.syncRuntimeAttributes({ chartInstance } as Partial); } else { diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts index 27383d378b..c0d0dceb67 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts @@ -88,6 +88,7 @@ export function createChartCellGroup( cellGroup.AABBBounds.width(); // TODO 需要底层VRender修改 // chart if ((isNoChartDataRenderNothing && Array.isArray(table.getCellValue(col, row))) || !isNoChartDataRenderNothing) { + const spec = table.options.specTransformInCell ? table.options.specTransformInCell(chartSpec, col, row) : chartSpec; const chartGroup = new Chart(isShareChartSpec, { stroke: false, x: padding[3], @@ -96,7 +97,7 @@ export function createChartCellGroup( canvas: table.canvas ?? (table.scenegraph.stage.window.getContext().canvas as unknown as HTMLCanvasElement), mode: table.options.mode, modeParams: table.options.modeParams, - spec: table.options.specTransformInCell ? table.options.specTransformInCell(chartSpec, col, row) : chartSpec, + spec, ClassType, width: width - padding[3] - padding[1], height: height - padding[2] - padding[0], @@ -121,7 +122,11 @@ export function createChartCellGroup( // }, tableChartOption: table.options.chartOption, col, - row + row, + deferRenderForTableConstructor: + table.isPivotChart() && + (table as any)._isConstructingPivotChart === true && + typeof spec?.label?.dataFilter === 'function' }); cellGroup.appendChild(chartGroup); // 将生成的实例存到layoutMap中 共享 From 89c5fcbc4a9cc1f5cbcb45fce1d4587770578964 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 13 Aug 2026 14:26:50 +0800 Subject: [PATCH 6/9] fix(vtable): defer only confirmed chart constructor retry errors Use a render-error predicate so chart constructor retries are limited to confirmed PivotChart construction timing failures instead of every label dataFilter render error. Co-Authored-By: Claude Sonnet 4.6 --- .../vtable/__tests__/chart-graphic.test.ts | 35 ++++++++++++++++++- .../vtable/src/scenegraph/graphic/chart.ts | 10 +++--- .../group-creater/cell-type/chart-cell.ts | 28 ++++++++++++--- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/packages/vtable/__tests__/chart-graphic.test.ts b/packages/vtable/__tests__/chart-graphic.test.ts index ae4204de30..50b4342eab 100644 --- a/packages/vtable/__tests__/chart-graphic.test.ts +++ b/packages/vtable/__tests__/chart-graphic.test.ts @@ -129,7 +129,7 @@ describe('Chart graphic', () => { axes: [], tableChartOption: {}, detectPickChartItem: false, - deferRenderForTableConstructor: true + shouldDeferRenderError: () => true } as any); }).not.toThrow(); @@ -207,4 +207,37 @@ describe('Chart graphic', () => { expect(chart).toBeUndefined(); expect(jest.getTimerCount()).toBe(0); }); + + test('throws constructor render errors synchronously when the defer predicate rejects them', () => { + jest.useFakeTimers(); + const canvas = document.createElement('canvas'); + + let chart: Chart | undefined; + expect(() => { + chart = new Chart(false, { + stroke: false, + x: 0, + y: 0, + width: 100, + height: 80, + canvas, + mode: 'desktop-browser', + modeParams: {}, + spec: { type: 'bar' }, + ClassType: MissingCellAddressChart, + chartInstance: undefined, + dataId: 'data', + data: [], + cellPadding: [0, 0, 0, 0], + dpr: 1, + axes: [], + tableChartOption: {}, + detectPickChartItem: false, + shouldDeferRenderError: () => false + } as any); + }).toThrow(GET_CELL_ADDRESS_ERROR_MESSAGE); + + expect(chart).toBeUndefined(); + expect(jest.getTimerCount()).toBe(0); + }); }); diff --git a/packages/vtable/src/scenegraph/graphic/chart.ts b/packages/vtable/src/scenegraph/graphic/chart.ts index 210a0384f8..c8f2b54f2a 100644 --- a/packages/vtable/src/scenegraph/graphic/chart.ts +++ b/packages/vtable/src/scenegraph/graphic/chart.ts @@ -43,7 +43,7 @@ interface IChartGraphicAttribute extends IGroupGraphicAttribute { col?: number; row?: number; detectPickChartItem?: boolean; - deferRenderForTableConstructor?: boolean; + shouldDeferRenderError?: (error: unknown) => boolean; } const CHART_RUNTIME_ATTRIBUTE_KEYS: (keyof IChartGraphicAttribute)[] = [ @@ -58,17 +58,17 @@ const CHART_RUNTIME_ATTRIBUTE_KEYS: (keyof IChartGraphicAttribute)[] = [ 'axes', 'tableChartOption', 'detectPickChartItem', - 'deferRenderForTableConstructor' + 'shouldDeferRenderError' ]; export const CHART_NUMBER_TYPE = genNumberType(); -function renderChartInstanceInConstructor(chartInstance: any, deferRenderForTableConstructor?: boolean) { +function renderChartInstanceInConstructor(chartInstance: any, shouldDeferRenderError?: (error: unknown) => boolean) { try { chartInstance.renderSync(); chartInstance.getStage().enableDirtyBounds(); } catch (error) { - if (!deferRenderForTableConstructor) { + if (!shouldDeferRenderError?.(error)) { throw error; } @@ -124,7 +124,7 @@ export class Chart extends Rect { autoFit: false }) )); - renderChartInstanceInConstructor(chartInstance, params.deferRenderForTableConstructor); + renderChartInstanceInConstructor(chartInstance, params.shouldDeferRenderError); params.chartInstance = this.chartInstance = chartInstance; this.syncRuntimeAttributes({ chartInstance } as Partial); } else { diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts index c0d0dceb67..4f0c2f94fe 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts @@ -89,6 +89,27 @@ export function createChartCellGroup( // chart if ((isNoChartDataRenderNothing && Array.isArray(table.getCellValue(col, row))) || !isNoChartDataRenderNothing) { const spec = table.options.specTransformInCell ? table.options.specTransformInCell(chartSpec, col, row) : chartSpec; + const data = table.getCellValue(col, row) || []; + const allowConstructorRenderRetry = + table.isPivotChart() && + (table as any)._isConstructingPivotChart === true && + typeof spec?.label?.dataFilter === 'function'; + const shouldDeferRenderError = allowConstructorRenderRetry + ? (error: unknown) => { + const errorLike = error as { name?: string; message?: string }; + if ( + errorLike?.name !== 'TypeError' || + typeof errorLike.message !== 'string' || + !errorLike.message.includes('getCellAddressByRecord') || + !errorLike.message.includes('undefined') || + !Array.isArray(data) + ) { + return false; + } + + return data.some(record => !!(table as any).getCellAddressByRecord?.(record)); + } + : undefined; const chartGroup = new Chart(isShareChartSpec, { stroke: false, x: padding[3], @@ -103,7 +124,7 @@ export function createChartCellGroup( height: height - padding[2] - padding[0], chartInstance, dataId, - data: table.getCellValue(col, row) || [], + data, cellPadding: padding, dpr: table.internalProps.pixelRatio, detectPickChartItem: table.options.customConfig?.detectPickChartItem, @@ -123,10 +144,7 @@ export function createChartCellGroup( tableChartOption: table.options.chartOption, col, row, - deferRenderForTableConstructor: - table.isPivotChart() && - (table as any)._isConstructingPivotChart === true && - typeof spec?.label?.dataFilter === 'function' + shouldDeferRenderError }); cellGroup.appendChild(chartGroup); // 将生成的实例存到layoutMap中 共享 From f2f8c381d4375f0c9b8ac2a449272f31f6b36d5b Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 13 Aug 2026 14:41:28 +0800 Subject: [PATCH 7/9] fix(vtable): cover chart retry lifecycle and local repro Clean deferred chart render retry timers on release and add a local bugserver reproduction plus production-path regression coverage. Co-Authored-By: Claude Sonnet 4.6 --- .../vtable/__tests__/chart-graphic.test.ts | 118 ++++++++++++++++++ .../debug/bugserver-6a7b16f-pivot-chart.ts | 91 ++++++++++++++ packages/vtable/examples/menu.ts | 4 + .../vtable/src/scenegraph/graphic/chart.ts | 33 ++++- 4 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 packages/vtable/examples/debug/bugserver-6a7b16f-pivot-chart.ts diff --git a/packages/vtable/__tests__/chart-graphic.test.ts b/packages/vtable/__tests__/chart-graphic.test.ts index 50b4342eab..a4783f2e98 100644 --- a/packages/vtable/__tests__/chart-graphic.test.ts +++ b/packages/vtable/__tests__/chart-graphic.test.ts @@ -1,4 +1,8 @@ import { Chart } from '../src/scenegraph/graphic/chart'; +import { createChartCellGroup } from '../src/scenegraph/group-creater/cell-type/chart-cell'; +import { Group } from '../src/scenegraph/graphic/group'; +import * as register from '../src/register'; +import { chartTypes } from '../src/chartModule'; const GET_CELL_ADDRESS_ERROR_MESSAGE = "Cannot destructure property 'col' of 'getCellAddressByRecord(...)' as it is undefined."; @@ -64,6 +68,7 @@ class MissingCellAddressChart extends ThrowOnceChart { describe('Chart graphic', () => { afterEach(() => { jest.useRealTimers(); + delete chartTypes['mock-chart']; }); test('keeps runtime refs when VRender builds static state snapshots', () => { @@ -142,6 +147,44 @@ describe('Chart graphic', () => { expect(chartInstance.renderCount).toBe(2); expect(chartInstance.dirtyBoundsCount).toBe(1); + expect(chart?.renderRetryTimer).toBeUndefined(); + }); + + test('clears deferred constructor render retry on release', () => { + jest.useFakeTimers(); + const canvas = document.createElement('canvas'); + + const chart = new Chart(false, { + stroke: false, + x: 0, + y: 0, + width: 100, + height: 80, + canvas, + mode: 'desktop-browser', + modeParams: {}, + spec: { type: 'bar' }, + ClassType: ThrowOnceChart, + chartInstance: undefined, + dataId: 'data', + data: [], + cellPadding: [0, 0, 0, 0], + dpr: 1, + axes: [], + tableChartOption: {}, + detectPickChartItem: false, + shouldDeferRenderError: () => true + } as any); + + const chartInstance = chart.chartInstance as ThrowOnceChart; + expect(chartInstance.renderCount).toBe(1); + expect(jest.getTimerCount()).toBe(1); + + chart.release(); + + expect(jest.getTimerCount()).toBe(0); + jest.runOnlyPendingTimers(); + expect(chartInstance.renderCount).toBe(1); }); test('throws non-recoverable constructor render errors synchronously', () => { @@ -240,4 +283,79 @@ describe('Chart graphic', () => { expect(chart).toBeUndefined(); expect(jest.getTimerCount()).toBe(0); }); + + test('uses createChartCellGroup production gates before deferring constructor render errors', () => { + jest.useFakeTimers(); + const canvas = document.createElement('canvas'); + const record = { indicator: 1 }; + const columnGroup = new Group({}); + register.chartModule('mock-chart', ThrowOnceChart); + const table = { + canvas, + colCount: 2, + rowCount: 2, + theme: { + cellInnerBorder: true, + frameStyle: {} + }, + options: { + mode: 'desktop-browser', + modeParams: {}, + chartOption: {} + }, + internalProps: { + pixelRatio: 1, + layoutMap: { + getChartAxes: () => [], + setChartInstance: jest.fn() + } + }, + scenegraph: { + stage: { + window: { + getContext: () => ({ canvas }) + } + } + }, + _isConstructingPivotChart: true, + _getCellStyle: () => ({}), + getCellValue: () => [record], + getCellAddressByRecord: jest.fn(value => (value === record ? { col: 1, row: 1 } : undefined)), + isPivotChart: () => true + }; + const cellTheme = { group: {} }; + + const cellGroup = createChartCellGroup( + null, + columnGroup, + 0, + 0, + 1, + 1, + 100, + 80, + [0, 0, 0, 0], + '', + 'mock-chart', + { type: 'bar', label: { dataFilter: () => [] } }, + undefined, + 'data', + table as any, + cellTheme as any, + true, + false, + false + ); + + const chart = cellGroup.lastChild as Chart; + const chartInstance = chart.chartInstance as ThrowOnceChart; + expect(chartInstance.renderCount).toBe(1); + expect(jest.getTimerCount()).toBe(1); + + jest.runOnlyPendingTimers(); + + expect(chartInstance.renderCount).toBe(2); + expect(table.getCellAddressByRecord).toHaveBeenCalledWith(record); + expect(table.internalProps.layoutMap.setChartInstance).toHaveBeenCalledWith(1, 1, chartInstance); + }); }); diff --git a/packages/vtable/examples/debug/bugserver-6a7b16f-pivot-chart.ts b/packages/vtable/examples/debug/bugserver-6a7b16f-pivot-chart.ts new file mode 100644 index 0000000000..1bebf7b6f4 --- /dev/null +++ b/packages/vtable/examples/debug/bugserver-6a7b16f-pivot-chart.ts @@ -0,0 +1,91 @@ +import * as VTable from '../../src'; +import { bindDebugTool } from '../../src/scenegraph/debug-tool'; + +const CONTAINER_ID = 'vTable'; +const GET_CELL_ADDRESS_ERROR_MESSAGE = + "Cannot destructure property 'col' of 'getCellAddressByRecord(...)' as it is undefined."; + +class BugserverChart { + spec: any; + + constructor(spec: any) { + this.spec = spec; + } + + renderSync() { + this.spec?.label?.dataFilter?.([{ data: this.spec.data.values[0] }]); + } + + getStage() { + return { + enableDirtyBounds() { + // noop + } + }; + } + + release() { + // noop + } +} + +VTable.register.chartModule('bugserver-chart', BugserverChart); + +export function createTable() { + const width = 600; + const height = width / 1.618; + let instance: VTable.PivotChart | undefined; + + const records = [ + { + '10001': '求和(qps)的总额百分比', + '10002': '0.001031658322960409623206456697', + '10003': '260622161148168', + '20001': '求和(qps)的总额百分比', + '260622161148168': '0.001031658322960409623206456697', + '260624121706060': '05_[0.5]', + '260811200516176': '直播' + } + ]; + + const option: VTable.PivotChartConstructorOptions = { + records, + rows: [], + columns: [{ dimensionKey: '260811200516176', title: '体裁' }], + indicatorsAsCol: false, + rowTree: [{ indicatorKey: '10002', value: '' }], + columnTree: [{ dimensionKey: '260811200516176', value: '直播' }], + indicators: [ + { + indicatorKey: '10002', + cellType: 'chart', + chartModule: 'bugserver-chart', + chartSpec: { + type: 'bar', + data: { + values: records + }, + label: { + dataFilter(labels: { data: Record }[]) { + const cellAddress = instance?.getCellAddressByRecord(labels[0].data); + if (!cellAddress) { + throw new TypeError(GET_CELL_ADDRESS_ERROR_MESSAGE); + } + return instance?.getCellValue(cellAddress.col, cellAddress.row) ?? labels; + } + } + } + } + ] + }; + + const dom = document.getElementById(CONTAINER_ID)!; + dom.style.width = `${width}px`; + dom.style.height = `${height}px`; + + instance = new VTable.PivotChart(dom, option); + instance.updateOption(option); + window.tableInstance = instance; + + bindDebugTool(instance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 5ea0fb6253..6aa95225fa 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -10,6 +10,10 @@ export const menus = [ path: 'debug', name: 'bugserver' }, + { + path: 'debug', + name: 'bugserver-6a7b16f-pivot-chart' + }, { path: 'debug', name: 'site' diff --git a/packages/vtable/src/scenegraph/graphic/chart.ts b/packages/vtable/src/scenegraph/graphic/chart.ts index c8f2b54f2a..5b3ca3f93e 100644 --- a/packages/vtable/src/scenegraph/graphic/chart.ts +++ b/packages/vtable/src/scenegraph/graphic/chart.ts @@ -63,7 +63,11 @@ const CHART_RUNTIME_ATTRIBUTE_KEYS: (keyof IChartGraphicAttribute)[] = [ export const CHART_NUMBER_TYPE = genNumberType(); -function renderChartInstanceInConstructor(chartInstance: any, shouldDeferRenderError?: (error: unknown) => boolean) { +function renderChartInstanceInConstructor( + chartInstance: any, + shouldDeferRenderError?: (error: unknown) => boolean, + onRenderRetryFinish?: () => void +) { try { chartInstance.renderSync(); chartInstance.getStage().enableDirtyBounds(); @@ -76,9 +80,13 @@ function renderChartInstanceInConstructor(chartInstance: any, shouldDeferRenderE throw error; } - setTimeout(() => { - chartInstance.renderSync(); - chartInstance.getStage().enableDirtyBounds(); + return setTimeout(() => { + try { + chartInstance.renderSync(); + chartInstance.getStage().enableDirtyBounds(); + } finally { + onRenderRetryFinish?.(); + } }, 0); } } @@ -93,6 +101,7 @@ export class Chart extends Rect { justShowMarkTooltip: boolean = undefined; justShowMarkTooltipTimer: number = Date.now(); delayRunDimensionHoverTimer: any = undefined; + renderRetryTimer: any = undefined; cacheCanvas: HTMLCanvasElement | { x: number; y: number; width: number; height: number; canvas: HTMLCanvasElement }[]; // HTMLCanvasElement isShareChartSpec: boolean; //针对chartSpec用户配置成函数形式的话 就不需要存储chartInstance了 会太占内存,使用这个变量 当渲染出缓存图表会就删除chartInstance实例 constructor(isShareChartSpec: boolean, params: IChartGraphicAttribute) { @@ -124,7 +133,9 @@ export class Chart extends Rect { autoFit: false }) )); - renderChartInstanceInConstructor(chartInstance, params.shouldDeferRenderError); + this.renderRetryTimer = renderChartInstanceInConstructor(chartInstance, params.shouldDeferRenderError, () => { + this.renderRetryTimer = undefined; + }); params.chartInstance = this.chartInstance = chartInstance; this.syncRuntimeAttributes({ chartInstance } as Partial); } else { @@ -623,6 +634,17 @@ export class Chart extends Rect { clearTimeout(this.delayRunDimensionHoverTimer); this.delayRunDimensionHoverTimer = undefined; } + + clearRenderRetryTimer() { + clearTimeout(this.renderRetryTimer); + this.renderRetryTimer = undefined; + } + + release(...args: any[]) { + this.clearRenderRetryTimer(); + return (super.release as any)(...args); + } + /** * 图表失去焦点 * @param table @@ -648,6 +670,7 @@ export class Chart extends Rect { this.justShowMarkTooltip = undefined; this.justShowMarkTooltipTimer = Date.now(); this.clearDelayRunDimensionHoverTimer(); + this.clearRenderRetryTimer(); if (releaseChartInstance) { // move active chart view box out of browser view // to avoid async render when chart is releasd From 1f97fa3d9b8681028c1ac07f6ccac60410b27e6e Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 13 Aug 2026 15:13:13 +0800 Subject: [PATCH 8/9] fix(vtable): keep bugserver pivot chart repro runnable Co-Authored-By: Claude Sonnet 4.6 --- .../vtable/__tests__/chart-graphic.test.ts | 4 +- .../debug/bugserver-6a7b16f-pivot-chart.ts | 102 ++++++++++++++++-- .../vtable/src/scenegraph/graphic/chart.ts | 3 +- .../group-creater/cell-type/chart-cell.ts | 3 +- 4 files changed, 102 insertions(+), 10 deletions(-) diff --git a/packages/vtable/__tests__/chart-graphic.test.ts b/packages/vtable/__tests__/chart-graphic.test.ts index a4783f2e98..30ba83b32d 100644 --- a/packages/vtable/__tests__/chart-graphic.test.ts +++ b/packages/vtable/__tests__/chart-graphic.test.ts @@ -306,7 +306,7 @@ describe('Chart graphic', () => { internalProps: { pixelRatio: 1, layoutMap: { - getChartAxes: () => [], + getChartAxes: (): any[] => [], setChartInstance: jest.fn() } }, @@ -337,7 +337,7 @@ describe('Chart graphic', () => { [0, 0, 0, 0], '', 'mock-chart', - { type: 'bar', label: { dataFilter: () => [] } }, + { type: 'bar', label: { dataFilter: (): any[] => [] } }, undefined, 'data', table as any, diff --git a/packages/vtable/examples/debug/bugserver-6a7b16f-pivot-chart.ts b/packages/vtable/examples/debug/bugserver-6a7b16f-pivot-chart.ts index 1bebf7b6f4..9bb61b42e2 100644 --- a/packages/vtable/examples/debug/bugserver-6a7b16f-pivot-chart.ts +++ b/packages/vtable/examples/debug/bugserver-6a7b16f-pivot-chart.ts @@ -7,6 +7,48 @@ const GET_CELL_ADDRESS_ERROR_MESSAGE = class BugserverChart { spec: any; + stage = { + viewWidth: 600, + viewHeight: 371, + window: { + dpr: 1, + getContext() { + return {}; + }, + getViewBoxTransform() { + return { + a: 1, + b: 0, + c: 0, + d: 1, + e: 0, + f: 0, + clone() { + return { + a: 1, + b: 0, + c: 0, + d: 1, + e: 0, + f: 0, + multiply() { + return this; + } + }; + } + }; + }, + setViewBoxTransform() { + // noop + } + }, + enableDirtyBounds() { + // noop + }, + renderTo() { + // noop + } + }; constructor(spec: any) { this.spec = spec; @@ -17,13 +59,57 @@ class BugserverChart { } getStage() { + return this.stage; + } + + getChart() { return { - enableDirtyBounds() { + setLayoutTag() { // noop } }; } + getSpec() { + return this.spec; + } + + updateViewBox() { + // noop + } + + updateDataSync() { + // noop + } + + updateFullDataSync() { + // noop + } + + updateSpecSync(spec: any) { + this.spec = spec; + } + + updateModelSpec() { + // noop + } + + updateModelSpecSync() { + // noop + } + + updateState() { + // noop + } + + on() { + // noop + } + + disableTooltip() { + // noop + } + release() { // noop } @@ -34,7 +120,7 @@ VTable.register.chartModule('bugserver-chart', BugserverChart); export function createTable() { const width = 600; const height = width / 1.618; - let instance: VTable.PivotChart | undefined; + const instanceRef: { current?: VTable.PivotChart } = {}; const records = [ { @@ -50,6 +136,7 @@ export function createTable() { const option: VTable.PivotChartConstructorOptions = { records, + disableInteraction: true, rows: [], columns: [{ dimensionKey: '260811200516176', title: '体裁' }], indicatorsAsCol: false, @@ -62,16 +149,18 @@ export function createTable() { chartModule: 'bugserver-chart', chartSpec: { type: 'bar', + xField: '260811200516176', + yField: '10002', data: { values: records }, label: { dataFilter(labels: { data: Record }[]) { - const cellAddress = instance?.getCellAddressByRecord(labels[0].data); + const cellAddress = instanceRef.current?.getCellAddressByRecord(labels[0].data); if (!cellAddress) { throw new TypeError(GET_CELL_ADDRESS_ERROR_MESSAGE); } - return instance?.getCellValue(cellAddress.col, cellAddress.row) ?? labels; + return instanceRef.current?.getCellValue(cellAddress.col, cellAddress.row) ?? labels; } } } @@ -83,9 +172,10 @@ export function createTable() { dom.style.width = `${width}px`; dom.style.height = `${height}px`; - instance = new VTable.PivotChart(dom, option); - instance.updateOption(option); + const instance = new VTable.PivotChart(dom, option); + instanceRef.current = instance; window.tableInstance = instance; + instance.updateOption(option); bindDebugTool(instance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); } diff --git a/packages/vtable/src/scenegraph/graphic/chart.ts b/packages/vtable/src/scenegraph/graphic/chart.ts index 5b3ca3f93e..a6fe189c8a 100644 --- a/packages/vtable/src/scenegraph/graphic/chart.ts +++ b/packages/vtable/src/scenegraph/graphic/chart.ts @@ -67,10 +67,11 @@ function renderChartInstanceInConstructor( chartInstance: any, shouldDeferRenderError?: (error: unknown) => boolean, onRenderRetryFinish?: () => void -) { +): ReturnType | undefined { try { chartInstance.renderSync(); chartInstance.getStage().enableDirtyBounds(); + return undefined; } catch (error) { if (!shouldDeferRenderError?.(error)) { throw error; diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts index 4f0c2f94fe..842f87f106 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts @@ -89,7 +89,8 @@ export function createChartCellGroup( // chart if ((isNoChartDataRenderNothing && Array.isArray(table.getCellValue(col, row))) || !isNoChartDataRenderNothing) { const spec = table.options.specTransformInCell ? table.options.specTransformInCell(chartSpec, col, row) : chartSpec; - const data = table.getCellValue(col, row) || []; + const cellValue = table.getCellValue(col, row); + const data: any[] = Array.isArray(cellValue) ? (cellValue as any[]) : []; const allowConstructorRenderRetry = table.isPivotChart() && (table as any)._isConstructingPivotChart === true && From d010241d072c217039d54206fafd0e3e188e3c88 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 13 Aug 2026 15:45:02 +0800 Subject: [PATCH 9/9] fix(vtable): use pivot records for chart retry gate Co-Authored-By: Claude Sonnet 4.6 --- packages/vtable/__tests__/chart-graphic.test.ts | 5 +++-- .../scenegraph/group-creater/cell-type/chart-cell.ts | 11 +++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/vtable/__tests__/chart-graphic.test.ts b/packages/vtable/__tests__/chart-graphic.test.ts index 30ba83b32d..0f0984fdc1 100644 --- a/packages/vtable/__tests__/chart-graphic.test.ts +++ b/packages/vtable/__tests__/chart-graphic.test.ts @@ -284,7 +284,7 @@ describe('Chart graphic', () => { expect(jest.getTimerCount()).toBe(0); }); - test('uses createChartCellGroup production gates before deferring constructor render errors', () => { + test('uses pivot chart records before deferring constructor render errors', () => { jest.useFakeTimers(); const canvas = document.createElement('canvas'); const record = { indicator: 1 }; @@ -319,7 +319,8 @@ describe('Chart graphic', () => { }, _isConstructingPivotChart: true, _getCellStyle: () => ({}), - getCellValue: () => [record], + records: [record], + getCellValue: (): any[] => [], getCellAddressByRecord: jest.fn(value => (value === record ? { col: 1, row: 1 } : undefined)), isPivotChart: () => true }; diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts index 842f87f106..61c05c99d6 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts @@ -91,6 +91,13 @@ export function createChartCellGroup( const spec = table.options.specTransformInCell ? table.options.specTransformInCell(chartSpec, col, row) : chartSpec; const cellValue = table.getCellValue(col, row); const data: any[] = Array.isArray(cellValue) ? (cellValue as any[]) : []; + const records = data.length + ? data + : Array.isArray((table as any).records) + ? ((table as any).records as any[]) + : Array.isArray((table.options as any)?.records) + ? ((table.options as any).records as any[]) + : []; const allowConstructorRenderRetry = table.isPivotChart() && (table as any)._isConstructingPivotChart === true && @@ -103,12 +110,12 @@ export function createChartCellGroup( typeof errorLike.message !== 'string' || !errorLike.message.includes('getCellAddressByRecord') || !errorLike.message.includes('undefined') || - !Array.isArray(data) + !records.length ) { return false; } - return data.some(record => !!(table as any).getCellAddressByRecord?.(record)); + return records.some(record => !!(table as any).getCellAddressByRecord?.(record)); } : undefined; const chartGroup = new Chart(isShareChartSpec, {