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,11 @@
{
"changes": [
{
"comment": "fix: avoid duplicate event handlers after chart remake",
"type": "patch",
"packageName": "@visactor/vchart"
}
],
"packageName": "@visactor/vchart",
"email": "lixuef1313@163.com"
}
82 changes: 82 additions & 0 deletions packages/vchart/__tests__/runtime/browser/event-update-spec.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VChart Event Update Spec Case</title>
<style>
html,
body {
margin: 0;
min-height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

body {
box-sizing: border-box;
padding: 24px;
background: #f8fafc;
}

.panel {
box-sizing: border-box;
max-width: 900px;
margin: 0 auto;
padding: 20px 24px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
}

h1 {
margin: 0 0 8px;
font-size: 20px;
}

p {
margin: 0 0 12px;
color: #475569;
line-height: 1.5;
}

.status {
display: flex;
gap: 24px;
margin-bottom: 12px;
font-variant-numeric: tabular-nums;
}

.status strong {
color: #0f172a;
}

#chart {
width: 100%;
height: 480px;
}

.actions {
display: flex;
gap: 12px;
align-items: center;
margin-top: 12px;
}
</style>
</head>
<body>
<main class="panel">
<h1>pointerdown 中 updateSpec 的事件注册用例</h1>
<p>连续点击任意柱子。每次点击都会更新被点击系列的 zIndex;回调次数应当每次只增加 1。</p>
<div class="status">
<span>pointerdown 回调次数:<strong id="eventCount">0</strong></span>
<span>最近命中系列:<strong id="targetIndex">-</strong></span>
</div>
<div id="chart"></div>
<div class="actions">
<button id="reset" type="button">重置用例</button>
<a href="./index.html">返回默认测试页</a>
</div>
</main>
<script type="module" src="./test-page/event-update-spec.ts"></script>
</body>
</html>
1 change: 1 addition & 0 deletions packages/vchart/__tests__/runtime/browser/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<!-- 添加控制面板和按钮 -->
<div id="controlPanel">
<button id="toggleScriptBtn">切换视图</button>
<a href="./event-update-spec.html">打开事件更新用例</a>
</div>

<!-- 图表将在此容器内创建 -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { default as VChart } from '../../../../src';

const CONTAINER_ID = 'chart';
const eventCountElement = document.getElementById('eventCount') as HTMLElement;
const targetIndexElement = document.getElementById('targetIndex') as HTMLElement;

const spec = {
type: 'common' as const,
data: [
{
id: 'A',
values: [
{ x: 'Q1', y: 45 },
{ x: 'Q2', y: 35 },
{ x: 'Q3', y: 20 }
]
},
{
id: 'B',
values: [
{ x: 'Q1', y: 35 },
{ x: 'Q2', y: 40 },
{ x: 'Q3', y: 45 }
]
},
{
id: 'C',
values: [
{ x: 'Q1', y: 20 },
{ x: 'Q2', y: 25 },
{ x: 'Q3', y: 35 }
]
}
],
series: [
{
type: 'bar' as const,
dataId: 'A',
xField: 'x',
yField: 'y',
stack: true,
bar: { style: { fill: '#22c55e' } }
},
{
type: 'bar' as const,
dataId: 'B',
xField: 'x',
yField: 'y',
stack: true,
zIndex: 2,
bar: {
style: {
fill: '#eab308',
outerBorder: { stroke: '#3370ff', lineWidth: 3, distance: 2 }
}
}
},
{
type: 'bar' as const,
dataId: 'C',
xField: 'x',
yField: 'y',
stack: true,
bar: { style: { fill: '#ef4444' } }
}
],
axes: [{ orient: 'left' as const }, { orient: 'bottom' as const, type: 'band' as const }]
};

const vchart = new VChart(spec, { dom: CONTAINER_ID, animation: false });
let eventCount = 0;

vchart.renderSync();

vchart.on('pointerdown', { markName: 'bar' }, event => {
const targetIndex = event.model.getSpecIndex();

spec.series.forEach((series, index) => {
if (index === targetIndex) {
series.zIndex = 2;
series.bar.style.outerBorder = { stroke: '#3370ff', lineWidth: 3, distance: 2 };
} else {
series.zIndex = 1;
series.bar.style.outerBorder = {
stroke: false
};
}
});

eventCount += 1;
eventCountElement.textContent = `${eventCount}`;
targetIndexElement.textContent = `${targetIndex}`;
console.log('pointerdown target index:', targetIndex);
vchart.updateSpec(spec);
});

document.getElementById('reset')?.addEventListener('click', () => {
window.location.reload();
});

// 仅用于在浏览器控制台检查实例,不是公共 API 使用示例。
window['vchart'] = vchart;
4 changes: 2 additions & 2 deletions packages/vchart/__tests__/unit/chart/bar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ describe('Bar chart test', () => {
{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
eventDispatcher: new EventDispatcher({} as never, { addEventListener: () => {} } as never),
eventDispatcher: new EventDispatcher({} as never, getTestCompiler()),
globalInstance: {
isAnimationEnable: () => true,
getContainer: () => ({}),
Expand Down Expand Up @@ -137,7 +137,7 @@ describe('Bar chart test', () => {
{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
eventDispatcher: new EventDispatcher({} as never, { addEventListener: () => {} } as never),
eventDispatcher: new EventDispatcher({} as never, getTestCompiler()),
globalInstance: {
isAnimationEnable: () => true,
getContainer: () => ({}),
Expand Down
131 changes: 131 additions & 0 deletions packages/vchart/__tests__/unit/core/vchart-event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,137 @@ describe('vchart event test', () => {
expect(pointDowmSpy).toBeCalledTimes(2);
});

it('should not duplicate a user event when its handler remakes the chart', async () => {
const eventContainer = createDiv();
const eventDom = createDiv(eventContainer);
const spec: ICommonChartSpec = {
type: 'common',
data: [
{
id: 'barData',
values: [
{ x: 'A', y: 10 },
{ x: 'B', y: 20 }
]
}
],
series: [
{
type: 'bar',
dataId: 'barData',
xField: 'x',
yField: 'y'
}
],
axes: [{ orient: 'left' }, { orient: 'bottom', type: 'band' }]
};
const chart = new VChart(spec, { dom: eventDom, animation: false });
const updatePromises: Promise<unknown>[] = [];
const pointerdownSpy = jest.fn(() => {
spec.series[0].zIndex = 1;
updatePromises.push(chart.updateSpec(spec));
});
const stage = chart.getStage();
const emitPointerdown = () => {
const listeners = (stage as unknown as { _events?: { pointerdown?: StageEventListener | StageEventListener[] } })
._events?.pointerdown;
const event = {
type: 'pointerdown',
target: stage,
defaultPrevented: false,
stopPropagation: jest.fn(),
preventDefault: jest.fn()
};
(listeners ? ('fn' in listeners ? [listeners] : listeners) : []).forEach((listener: StageEventListener) => {
listener.fn.call(listener.context, event);
});
};

try {
chart.renderSync();
chart.on('pointerdown', pointerdownSpy);

emitPointerdown();
await Promise.all(updatePromises.splice(0));
emitPointerdown();
await Promise.all(updatePromises.splice(0));

expect(pointerdownSpy).toBeCalledTimes(2);
} finally {
chart.release();
removeDom(eventContainer);
}
});

it('should release chart-owned interaction event handlers before remake', () => {
const eventContainer = createDiv();
const eventDom = createDiv(eventContainer);
const spec: ICommonChartSpec = {
type: 'common',
data: [
{
id: 'barData',
values: [
{ x: 'A', y: 10 },
{ x: 'B', y: 20 }
]
}
],
series: [
{
type: 'bar',
dataId: 'barData',
xField: 'x',
yField: 'y',
hover: false,
select: false,
bar: {
state: {
active: {
fillOpacity: 0.5
}
}
},
interactions: [
{
type: 'element-active',
trigger: 'pointerover',
triggerOff: 'none'
}
]
}
],
axes: [{ orient: 'left' }, { orient: 'bottom', type: 'band' }]
};
const chart = new VChart(spec, { dom: eventDom, animation: false });
const getPointeroverHandlerCount = () => {
const eventDispatcher = chart as unknown as {
_eventDispatcher: { _viewBubbles: Map<string, { getCount: () => number }> };
};

return eventDispatcher._eventDispatcher._viewBubbles.get('pointerover')?.getCount();
};

try {
chart.renderSync();
expect(getPointeroverHandlerCount()).toBe(1);

spec.series[0].zIndex = 1;
chart.updateSpecSync(spec);

expect(getPointeroverHandlerCount()).toBe(1);
const barSeries = (chart.getChart() as IChart).getAllSeries()[0];
const barMark = barSeries.getMarks().find(mark => mark.name === 'bar') as IMark;
const barGraphic = barMark.getGraphics()[0] as IMarkGraphic;

(chart.getChart() as IChart).getEvent().emit('pointerover', { item: barGraphic } as unknown as BaseEventParams);
expect(barGraphic.hasState('active')).toBe(true);
} finally {
chart.release();
removeDom(eventContainer);
}
});

it('should keep tooltip and crosshair triggerable after line mark and marker update without remake', () => {
const lineContainer = createDiv();
const lineDom = createDiv(lineContainer);
Expand Down
2 changes: 2 additions & 0 deletions packages/vchart/__tests__/util/factory/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const getTestCompiler = () =>
updateLayoutTag: () => {},
getStage: getTestStage,
addRootMark: () => {},
addEventListener: () => {},
removeEventListener: () => {},
renderNextTick: () => {},
addGrammarItem: () => {}
} as any);
1 change: 1 addition & 0 deletions packages/vchart/src/chart/base/base-chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1571,6 +1571,7 @@ export class BaseChart<T extends IChartSpec> extends CompilableBase implements I
}

/* 开始 release */
this._event.release();
super.release();
// clear event , temporary function of chart items
this.clear();
Expand Down
Loading
Loading