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,21 @@
{
"changes": [
{
"packageName": "@visactor/vchart",
"comment": "fix: rebuild background when spec.background and spec.theme change in the same updateSpec",
"type": "patch"
},
{
"packageName": "@visactor/vchart",
"comment": "fix: avoid crash when recompiling a mark without a compile option",
"type": "patch"
},
{
"packageName": "@visactor/vchart",
"comment": "fix: remove the word cloud shape afterRender tap when the series is released",
"type": "patch"
}
],
"packageName": "@visactor/vchart",
"email": "chendaxin.tk@bytedance.com"
}
100 changes: 100 additions & 0 deletions packages/vchart/__tests__/unit/core/update-spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3990,3 +3990,103 @@ describe('vchart updateSpec of different indicator', () => {
});
});
});

describe('vchart updateSpec of background', () => {
let container: HTMLElement;
let dom: HTMLElement;
let vchart: VChart;

const gradientBackground = {
fill: {
gradient: 'linear',
x0: 0,
y0: 0,
x1: 1,
y1: 0,
stops: [
{ offset: 0, color: '#ff9f43' },
{ offset: 1, color: '#ee5253' }
]
}
};

const baseSpec = {
type: 'bar',
data: [
{
id: 'bar',
values: [
{ x: 'A', y: 10, type: 'a' },
{ x: 'B', y: 20, type: 'a' }
]
}
],
xField: 'x',
yField: 'y',
seriesField: 'type',
legends: [{ visible: true }],
animation: false
};

beforeAll(() => {
container = createDiv();
dom = createDiv(container);
dom.id = 'container';
container.style.position = 'fixed';
container.style.width = '500px';
container.style.height = '500px';
container.style.top = '0px';
container.style.left = '0px';
});

afterEach(() => {
vchart?.release();
});

afterAll(() => {
removeDom(container);
});

it('should remake background when background and theme change in the same updateSpec', () => {
const spec = {
...baseSpec,
background: gradientBackground,
theme: { background: 'transparent' }
} as unknown as IBarChartSpec;

vchart = new VChart(spec, { dom, animation: false });
vchart.renderSync();

const updateRes = (vchart as any)._updateSpec(
{
...spec,
background: '#4e83fd',
theme: { background: '#ffffff' }
},
false
);

expect(updateRes.changeTheme).toBe(true);
expect(updateRes.changeBackground).toBe(true);
expect(updateRes.reMake).toBe(true);
});

it('should not throw when recompiling a chart whose background is a mark spec', () => {
const spec = {
...baseSpec,
background: gradientBackground,
theme: { background: 'transparent' }
} as unknown as IBarChartSpec;

vchart = new VChart(spec, { dom, animation: false });
vchart.renderSync();

// 只改图例:走 reCompile 而不是 reMake,chart 级的 background mark 会被二次 compile
const nextSpec = { ...spec, legends: [{ visible: false }] } as unknown as IBarChartSpec;
const updateRes = (vchart as any)._updateSpec(nextSpec, false);
expect(updateRes.reCompile).toBe(true);
expect(updateRes.reMake).toBe(false);

expect(() => (vchart as any)._updateCustomConfigAndRecompile(updateRes)).not.toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { VChart } from '../../../src/vchart-all';
import { createDiv, removeDom } from '../../util/dom';

type Tap = { name: string; fn: () => void };

const SVG_MASK =
'<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><circle cx="50" cy="50" r="50"/></svg>';

const spec = {
type: 'wordCloud',
nameField: 'name',
valueField: 'value',
maskShape: SVG_MASK,
data: [
{
id: 'wordCloud',
values: [
{ name: 'foo', value: 30 },
{ name: 'bar', value: 20 },
{ name: 'baz', value: 10 }
]
}
],
animation: false
};

describe('word cloud shape afterRender tap', () => {
let container: HTMLElement;
let dom: HTMLElement;
let vchart: VChart;

const getWordCloudTaps = () =>
((vchart.getStage() as any).hooks.afterRender.taps as Tap[]).filter(tap => tap.name === 'afterWordcloudShapeDraw');

const createChart = () => {
vchart = new VChart(spec as any, { dom, animation: false });
vchart.renderSync();
return vchart.getChart().getAllSeries()[0] as any;
};

beforeAll(() => {
container = createDiv();
dom = createDiv(container);
dom.id = 'container';
container.style.position = 'fixed';
container.style.width = '500px';
container.style.height = '500px';
container.style.top = '0px';
container.style.left = '0px';
});

afterEach(() => {
vchart?.release();
});

afterAll(() => {
removeDom(container);
});

it('should be removed together with the series', () => {
const series = createChart();
series._wordCloudShapeTransformOption().onLayoutFinished();
expect(getWordCloudTaps().length).toBe(1);

const tap = getWordCloudTaps()[0];
series.release();

expect(getWordCloudTaps().length).toBe(0);
// series 已经 release,_option 为空,这个 tap 即使被别处留住也不能再抛错
expect(() => tap.fn()).not.toThrow();
});

it('should not be registered twice when the layout finishes more than once', () => {
const series = createChart();
const option = series._wordCloudShapeTransformOption();
option.onLayoutFinished();
option.onLayoutFinished();

expect(getWordCloudTaps().length).toBe(1);
});
});
4 changes: 3 additions & 1 deletion packages/vchart/src/core/vchart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1172,7 +1172,9 @@ export class VChart implements IVChart {
// setCurrentTheme 会导致 chart 实例的 reInit。
// 只要模块从 vchart 实例获取与 spec 相关的信息,都会出现错误,它们已经不匹配了
// this._setCurrentTheme();
} else if (!isEqual(this._spec.background, lastSpec.background)) {
}
// background 与 theme 互相独立,同一次 updateSpec 里两者一起变时也要重建背景 mark
if (!isEqual(this._spec.background, lastSpec.background)) {
result.reMake = true;
result.changeBackground = true;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vchart/src/mark/base/base-mark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar
}
return;
} else if (isValid(product)) {
if (option.group && product.parent !== option.group) {
if (option?.group && product.parent !== option.group) {
option.group.appendChild(product);
}
} else {
Expand Down
45 changes: 32 additions & 13 deletions packages/vchart/src/series/word-cloud/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { LinearScale } from '@visactor/vscale';
import type { GeometricMaskShape, TextShapeMask } from '@visactor/vlayouts';
import type { ITransformSpec } from '../../compile/interface';
import { createImage, vglobal } from '../../vrender-bridge';
import type { IStage } from '@visactor/vrender-core';
import { getTextBounds } from '@visactor/vrender-core/text';
import { wordCloud } from '../../theme/builtin/common/series/word-cloud';
import { LayoutZIndex } from '../../constant/layout';
Expand Down Expand Up @@ -78,6 +79,8 @@ export class BaseWordCloudSeries<T extends IBaseWordCloudSeriesSpec = IBaseWordC

protected _maskShape?: string | WordCloudShapeType | TextShapeMask | GeometricMaskShape;
protected _isWordCloudShape: boolean = false;
/** 形状词云布局完成后挂到 stage 上的 afterRender tap,随 series 一起摘掉,避免 release 后再被触发 */
protected _afterWordcloudShapeDrawTap?: { stage: IStage; fn: () => void };

protected _wordCloudConfig?: WordCloudConfigType;
protected _wordCloudShapeConfig?: WordCloudShapeConfigType;
Expand Down Expand Up @@ -394,25 +397,29 @@ export class BaseWordCloudSeries<T extends IBaseWordCloudSeriesSpec = IBaseWordC
: this._maskShape,
onUpdateMaskCanvas: this.handleMaskCanvasUpdate,
onLayoutFinished: () => {
// 布局是异步的,跑完时 series 可能已经被 release,此时 _option 已置空
const stage = this._option?.globalInstance?.getStage();
if (!stage) {
return;
}
this._removeAfterWordcloudShapeDrawTap();

const afterWordcloudShapeDraw = () => {
// 需要等到真正渲染完成
this._option.globalInstance.getStage().hooks.afterRender.taps = this._option.globalInstance
.getStage()
.hooks.afterRender.taps.filter(tap => tap.fn !== afterWordcloudShapeDraw);
this._removeAfterWordcloudShapeDrawTap();

const globalInstance = this._option?.globalInstance;
if (!globalInstance) {
return;
}
this._option.dispatchEvent?.(ChartEvent.afterWordcloudShapeDraw, {
instance: this._option.globalInstance
instance: globalInstance
});
this._option.globalInstance
.getChart()
.getOption()
.performanceHook?.afterWordcloudShapeDraw?.(this._option.globalInstance);
globalInstance.getChart().getOption().performanceHook?.afterWordcloudShapeDraw?.(globalInstance);
};
this._option.globalInstance.getStage().hooks.afterRender.taps.push({
type: 'sync',
name: 'afterWordcloudShapeDraw',
fn: afterWordcloudShapeDraw
});

this._afterWordcloudShapeDrawTap = { stage, fn: afterWordcloudShapeDraw };
stage.hooks.afterRender.tap('afterWordcloudShapeDraw', afterWordcloudShapeDraw);
},
dataIndexKey: DEFAULT_DATA_KEY,
text: wordSpec.formatMethod
Expand Down Expand Up @@ -552,7 +559,19 @@ export class BaseWordCloudSeries<T extends IBaseWordCloudSeriesSpec = IBaseWordC
this._wordMeasureCache?.clear();
}

protected _removeAfterWordcloudShapeDrawTap() {
const tap = this._afterWordcloudShapeDrawTap;
if (!tap) {
return;
}
this._afterWordcloudShapeDrawTap = undefined;

// 带上 fn:同一个 stage 上可能有多个词云系列,只按名字清会误删别人的回调
tap.stage?.hooks?.afterRender?.unTap('afterWordcloudShapeDraw', tap.fn);
}

release() {
this._removeAfterWordcloudShapeDrawTap();
super.release();
this._wordMeasureCache?.clear();
this._wordMeasureCache = undefined;
Expand Down
Loading