Skip to content
Merged
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
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@visactor/vchart",
"comment": "修复默认 autoFit 的首次或重复尺寸通知覆盖手动 resize 的问题,同时保持 Spec 重建后的画布尺寸及真实容器变化的自动适配。",
"type": "patch"
}
],
"packageName": "@visactor/vchart"
}
99 changes: 99 additions & 0 deletions packages/vchart/__tests__/unit/core/resize-remake.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,102 @@ test('manual resize survives spec remakes and keeps the chart and stage at the s
removeDom(dom);
}
});

describe('manual resize with autoFit enabled', () => {
const resizeObserverDescriptor = Object.getOwnPropertyDescriptor(window, 'ResizeObserver');
let dom: HTMLElement;
let chart: VChart;
let notifyResize: () => void;

beforeEach(() => {
dom = createDiv();
dom.style.width = '600px';
dom.style.height = '600px';
// 控制首次和后续容器通知,避免浏览器调度顺序影响尺寸契约测试。
Object.defineProperty(window, 'ResizeObserver', {
configurable: true,
writable: true,
value: jest.fn().mockImplementation(callback => {
const observer = { observe: jest.fn(), unobserve: jest.fn(), disconnect: jest.fn() };
notifyResize = () => callback([], observer);
return observer;
})
});
});

afterEach(() => {
chart.release();
removeDom(dom);
if (resizeObserverDescriptor) {
Object.defineProperty(window, 'ResizeObserver', resizeObserverDescriptor);
} else {
delete (window as any).ResizeObserver;
}
});

const createChart = (spec: IBarChartSpec) => {
chart = new VChart(spec, { dom, animation: false, resizeDelay: 0, dpr: 1 });
chart.renderSync();
};

const expectSize = (width: number, height: number) => {
expect(chart.getChart()!.getCanvasRect()).toEqual({ width, height });
expect([chart.getStage()!.width, chart.getStage()!.height]).toEqual([width, height]);
expect([chart.getCanvas()!.width, chart.getCanvas()!.height]).toEqual([width, height]);
};

test('initial observer delivery and unchanged window resize preserve the manual size', async () => {
createChart({ ...makeSpec(), width: 600, height: 600 });
await chart.resize(300, 300);
notifyResize();
expectSize(300, 300);
window.dispatchEvent(new Event('resize'));
expectSize(300, 300);
});

test('real container changes still resize the chart after a manual resize', () => {
createChart(makeSpec());
chart.resizeSync(300, 300);
notifyResize();
expectSize(300, 300);
dom.style.width = '480px';
dom.style.height = '360px';
notifyResize();
expectSize(480, 360);
chart.resizeSync(250, 200);
window.dispatchEvent(new Event('resize'));
expectSize(250, 200);
});

test('an automatic size equal to the current canvas still advances the observed size', () => {
createChart(makeSpec());
chart.resizeSync(300, 300);
dom.style.width = '300px';
dom.style.height = '300px';
notifyResize();
expectSize(300, 300);
chart.resizeSync(250, 200);
notifyResize();
expectSize(250, 200);
dom.style.width = '600px';
dom.style.height = '600px';
notifyResize();
expectSize(600, 600);
});

test('remakes preserve the manual size and new explicit spec dimensions reset automatic sizing', () => {
createChart({ ...makeSpec(), width: 600, height: 600 });
chart.resizeSync(300, 300);
const original = chart.getChart();
chart.updateSpecSync({ ...makeSpec(true), width: 600, height: 600 });
expect(chart.getChart()).not.toBe(original);
notifyResize();
expectSize(300, 300);
chart.updateSpecSync({ ...makeSpec(), width: 640, height: 480 });
expectSize(640, 480);
notifyResize();
chart.resizeSync(320, 240);
window.dispatchEvent(new Event('resize'));
expectSize(320, 240);
});
});
89 changes: 89 additions & 0 deletions packages/vchart/__tests__/visual/cases/api/resize-auto-fit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* 验证目的:默认 autoFit 的首次容器通知不能撤销手动 resize。
* 图表类型:bar。
* 关键配置:显式 600×600、默认 autoFit、resize(300, 300)、customMark 触发重建。
* 场景条件:同一浏览器任务内 renderAsync 后立即 resize,再等待真实 ResizeObserver 通知。
* 最终检查:resize、首次通知、无尺寸变化的窗口事件及重建后,布局、stage 和 Canvas 均为 300×300。
* 覆盖边界:容器真实变化和新显式 Spec 尺寸覆盖由核心单元测试验证。
*/
export default {
createSpec() {
return {
type: 'bar',
width: 600,
height: 600,
xField: 'date',
yField: 'value',
data: {
id: 'data',
values: [
{ date: '2019-08-29', value: 39359021311 },
{ date: '2019-08-30', value: 41363548585 }
]
},
tooltip: { transitionDuration: 0 }
};
},
async exercise(page) {
// 重新创建实例,让首次原生 observer 通知发生在手动 resize 之后。
await page.evaluate(async () => {
const spec = window.__visualChart.getSpec();
window.__visualChart.release();
const chart = (window.__visualChart = new window.VChart.default(spec, {
dom: 'chart',
animation: false,
resizeDelay: 0,
theme: { fontFamily: 'Arial', tooltip: { transitionDuration: 0 } },
onError: error => window.__visualErrors.push(String(error))
}));
const sizes = (window.__resizeAutoFitSizes = []);
const capture = () => {
const rect = chart.getChart().getCanvasRect();
const stage = chart.getStage();
const canvas = chart.getCanvas();
sizes.push([rect.width, rect.height, stage.width, stage.height, canvas.width, canvas.height]);
};
// 测试自己的原生 observer 提供交付屏障,不以固定延时猜测首次通知完成时间。
const delivered = new Promise(resolve => {
const observer = new ResizeObserver(() => {
observer.disconnect();
requestAnimationFrame(resolve);
});
observer.observe(document.getElementById('chart'));
});
await chart.renderAsync();
await chart.resize(300, 300);
capture();
await delivered;
capture();
window.dispatchEvent(new Event('resize'));
capture();
const original = chart.getChart();
await chart.updateSpec({
...chart.getSpec(),
customMark: [{ type: 'text', style: { text: 'resize 300×300', x: 20, y: 20 } }]
});
window.__resizeAutoFitRemade = chart.getChart() !== original;
window.dispatchEvent(new Event('resize'));
capture();
});
},
async verify(page) {
// 每个阶段检查真实尺寸,并确认重建确实发生,防止无效动作产生假通过。
await page.evaluate(() => {
const sizes = window.__resizeAutoFitSizes;
const chart = window.__visualChart;
const rect = chart.getChart().getCanvasRect();
const stage = chart.getStage();
const canvas = chart.getCanvas();
if (
!window.__resizeAutoFitRemade ||
sizes?.length !== 4 ||
sizes.some(size => size.some(value => value !== 300)) ||
[rect.width, rect.height, stage.width, stage.height, canvas.width, canvas.height].some(value => value !== 300)
) {
throw new Error('自动尺寸监听或 Spec 重建覆盖了手动 resize 的 300×300 尺寸:' + JSON.stringify(sizes));
}
});
}
};
5 changes: 5 additions & 0 deletions packages/vchart/__tests__/visual/cases/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ export const cases = [
file: './api/update-resize.mjs',
sourceExample: 'packages/vchart/__tests__/runtime/browser/test-page/event-update-spec.ts'
},
{
id: 'resize-auto-fit',
purpose: '默认自动尺寸监听不覆盖手动 resize,Spec 重建后保持画布尺寸',
file: './api/resize-auto-fit.mjs'
},
{
id: 'bar-area-smooth',
purpose: '同一区域的柱图与平滑轮廓面积图叠加',
Expand Down
12 changes: 8 additions & 4 deletions packages/vchart/src/core/vchart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,9 @@ export class VChart implements IVChart {
}
};

private _currentSize: { width: number; height: number };
private _currentSize: Readonly<{ width: number; height: number }>;
// 自动尺寸来源的快照;手动 resize 只改变当前画布,不能使未变化的来源被误判为变化。
private _lastAutoSize: Readonly<{ width: number; height: number }>;
private _observer: ResizeObserver = null;

private _currentThemeName: string;
Expand Down Expand Up @@ -419,6 +421,7 @@ export class VChart implements IVChart {
this._setNewSpec(spec);
this._updateCurrentTheme();
this._currentSize = this.getCurrentSize();
this._lastAutoSize = this._currentSize;
const pluginList: string[] = [];
const runtimePluginInstallers: ((app?: IApp) => void)[] = [];

Expand Down Expand Up @@ -630,8 +633,9 @@ export class VChart implements IVChart {

private _doResize = () => {
const { width, height } = this.getCurrentSize();
if (this._currentSize.width !== width || this._currentSize.height !== height) {
if (this._lastAutoSize.width !== width || this._lastAutoSize.height !== height) {
this._currentSize = { width, height };
this._lastAutoSize = this._currentSize;
this.resizeSync(width, height);
}
};
Expand Down Expand Up @@ -740,6 +744,7 @@ export class VChart implements IVChart {
const { width, height } = this.getCurrentSize();

this._currentSize = { width, height };
this._lastAutoSize = this._currentSize;
this._chart?.onResize(width, height, false);
this._compiler?.resize(width, height, false);
}
Expand Down Expand Up @@ -1331,8 +1336,7 @@ export class VChart implements IVChart {

this._option.performanceHook?.beforeResizeWithUpdate?.(this);
// updateSpec 重建 chart 时沿用当前尺寸,不能恢复到手动 resize 之前的画布大小。
this._currentSize.width = width;
this._currentSize.height = height;
this._currentSize = { width, height };
this._chart.onResize(width, height, false);
this._option.performanceHook?.afterResizeWithUpdate?.(this);

Expand Down
Loading