diff --git a/src/chart/controller/annotation.ts b/src/chart/controller/annotation.ts index 743990ccd6..f8e6a741a9 100644 --- a/src/chart/controller/annotation.ts +++ b/src/chart/controller/annotation.ts @@ -19,7 +19,7 @@ import { } from '../../interface'; import { DEFAULT_ANIMATE_CFG } from '../../animate/'; -import { COMPONENT_TYPE, DIRECTION, LAYER, VIEW_LIFE_CIRCLE } from '../../constant'; +import { COMPONENT_TYPE, DIRECTION, GEOMETRY_LIFE_CIRCLE, LAYER, VIEW_LIFE_CIRCLE } from '../../constant'; import Geometry from '../../geometry/base'; import Element from '../../geometry/element'; @@ -63,6 +63,7 @@ export default class Annotation extends Controller { const theme = this.getAnnotationTheme(type); component.update(this.getAnnotationCfg(type, extra, theme)); + component.render(); }; const createComponentFn = (option: BaseOption) => { const co = this.createAnnotation(option); @@ -84,7 +85,7 @@ export default class Annotation extends Controller { if (component.get('type') === 'regionFilter') { // regionFilter 依赖绘制后的 Geometry Shapes - this.view.getRootView().once(VIEW_LIFE_CIRCLE.AFTER_RENDER, () => { + this.whenRegionFilter(() => { updateComponentFn(co); }); } else { @@ -94,8 +95,7 @@ export default class Annotation extends Controller { } else { each(this.option, (option: BaseOption) => { if (option.type === 'regionFilter') { - this.view.getRootView().once(VIEW_LIFE_CIRCLE.AFTER_RENDER, () => { - // regionFilter 依赖绘制后的 Geometry Shapes + this.whenRegionFilter(() => { createComponentFn(option); }); } else { @@ -147,7 +147,7 @@ export default class Annotation extends Controller { } }; - this.view.once(VIEW_LIFE_CIRCLE.AFTER_RENDER, () => { + this.whenRegionFilter(() => { // 先看是否有 regionFilter 要更新 each(this.option, (option: BaseOption) => { if (option.type === 'regionFilter') { @@ -215,6 +215,27 @@ export default class Annotation extends Controller { return co; } + /** + * region filter 比较特殊的渲染时机 + * @param doWhat + */ + private whenRegionFilter(doWhat: () => void) { + if (this.view.getOptions().animate) { + this.view.geometries.forEach((g: Geometry) => { + // 如果 geometry 开启,则监听 + if (g.animateOption) { + g.once(GEOMETRY_LIFE_CIRCLE.AFTER_DRAW_ANIMATE, () => { + doWhat(); + }); + } + }) + } else { + this.view.getRootView().once(VIEW_LIFE_CIRCLE.AFTER_RENDER, () => { + doWhat(); + }); + } + } + private createAnnotation(option: BaseOption) { const { type } = option; diff --git a/src/constant.ts b/src/constant.ts index 768e1371e4..45b09445ef 100644 --- a/src/constant.ts +++ b/src/constant.ts @@ -86,6 +86,14 @@ export enum VIEW_LIFE_CIRCLE { AFTER_CHANGE_SIZE = 'afterchangesize', } +/** + * geometry 的生命周期 + */ +export enum GEOMETRY_LIFE_CIRCLE { + BEFORE_DRAW_ANIMATE = 'beforeanimate', + AFTER_DRAW_ANIMATE = 'afteranimate', +} + /** * 绘图区的事件列表 */ diff --git a/src/geometry/element/index.ts b/src/geometry/element/index.ts index c342c8c48b..7b2365c32f 100644 --- a/src/geometry/element/index.ts +++ b/src/geometry/element/index.ts @@ -1,12 +1,12 @@ import { deepMix, each, get, isArray, isFunction, isString } from '@antv/util'; +import { propagationDelegate } from '@antv/component/lib/util/event'; import { doAnimate } from '../../animate'; import Base from '../../base'; import { BBox, IGroup, IShape } from '../../dependents'; import { AnimateOption, Datum, ShapeFactory, ShapeInfo, StateCfg } from '../../interface'; import { getReplaceAttrs } from '../../util/graphics'; import Geometry from '../base'; - -import { propagationDelegate } from '@antv/component/lib/util/event'; +import { GEOMETRY_LIFE_CIRCLE } from '../../constant'; /** Element 构造函数传入参数类型 */ interface ElementCfg { @@ -361,7 +361,19 @@ export default class Element extends Base { private getAnimateCfg(animateType: string) { const animate = this.animate; if (animate) { - return animate[animateType]; + const cfg = animate[animateType]; + + if (cfg) { + // 增加动画的回调函数,如果外部传入了,则先执行外部,然后发射 geometry 的 animate 事件 + return { + ...cfg, + callback: () => { + isFunction(cfg.callback) && cfg.callback(); + this.geometry?.emit(GEOMETRY_LIFE_CIRCLE.AFTER_DRAW_ANIMATE); + }, + } + } + return cfg; } return null; @@ -391,6 +403,9 @@ export default class Element extends Base { const animateType = isUpdate ? 'enter' : 'appear'; const animateCfg = this.getAnimateCfg(animateType); if (animateCfg) { + // 开始执行动画的生命周期 + this.geometry?.emit(GEOMETRY_LIFE_CIRCLE.BEFORE_DRAW_ANIMATE); + doAnimate(this.shape, animateCfg, { coordinate: shapeFactory.coordinate, toAttrs: { @@ -461,6 +476,7 @@ export default class Element extends Base { if (this.animate) { if (animateCfg) { + this.geometry?.emit(GEOMETRY_LIFE_CIRCLE.BEFORE_DRAW_ANIMATE); // 需要进行动画 doAnimate(sourceShape, animateCfg, { coordinate: this.shapeFactory.coordinate, diff --git a/tests/bugs/2851-spec.ts b/tests/bugs/2851-spec.ts new file mode 100644 index 0000000000..d474d9e123 --- /dev/null +++ b/tests/bugs/2851-spec.ts @@ -0,0 +1,61 @@ +import { Chart } from '../../src'; +import { createDiv } from '../util/dom'; +import { delay } from '../util/delay'; + +describe('2851', () => { + it('2851', async () => { + const data = [ + { year: '1951 年', sales: 280 }, + { year: '1952 年', sales: 52 }, + { year: '1956 年', sales: 61 }, + { year: '1957 年', sales: 145 }, + { year: '1958 年', sales: 48 }, + { year: '1959 年', sales: 38 }, + { year: '1960 年', sales: 38 }, + { year: '1962 年', sales: 38 }, + ]; + + const chart = new Chart({ + container: createDiv(), + width: 400, + height: 300, + autoFit: true, + }); + + chart.animate(false); + chart.data(data); + + chart + .line() + .position('year*sales'); + + chart.annotation().line({ + top: true, + start: ['min', 100], + end: ['max', 100], + style: { + stroke: 'red', + lineDash: [2, 2], + }, + }); + + chart.annotation().regionFilter({ + top: true, + start: ['min', 100], + end: ['max', 0], + color: '#f5222d' + }); + + chart.render(); + // 防止事件内存泄露 + // @ts-ignore + expect(chart.geometries[0]._events).toEqual({}); + + chart.changeSize(500, 400); + + // @ts-ignore + expect(chart.geometries[0]._events).toEqual({}); + + // regionFilter 不知道怎么去断言! + }) +}); diff --git a/tests/bugs/sub-view-region-filter-spec.ts b/tests/bugs/sub-view-region-filter-spec.ts index 6dbcb6b9b8..9df14107f8 100644 --- a/tests/bugs/sub-view-region-filter-spec.ts +++ b/tests/bugs/sub-view-region-filter-spec.ts @@ -32,6 +32,8 @@ describe('#0000', () => { padding: [20, 40, 0, 30], }); + view.animate(false); + // Step 2: 载入数据源 view.data(data); view.scale({ diff --git a/tests/unit/component/annotation-spec.ts b/tests/unit/component/annotation-spec.ts index 290d8f7ff4..0ec128e8c3 100644 --- a/tests/unit/component/annotation-spec.ts +++ b/tests/unit/component/annotation-spec.ts @@ -2,6 +2,7 @@ import 'jest-extended'; import { Chart } from '../../../src/'; import { COMPONENT_TYPE } from '../../../src/constant'; import { createDiv, removeDom } from '../../util/dom'; +import { delay } from '../../util/delay'; const IMAGE = 'https://img.alicdn.com/tfs/TB1M.wKkND1gK0jSZFyXXciOVXa-120-120.png'; @@ -297,7 +298,7 @@ describe('annotation', () => { expect(dataRegion.getElementById('-annotation-text-bg')).toBeDefined(); }); - it('regionFilter', () => { + it('regionFilter', async () => { chart.line().position('city*sale'); chart.annotation().regionFilter({ start: { city: '广州', sale: 30 }, @@ -307,6 +308,8 @@ describe('annotation', () => { }); chart.render(); + await delay(700); + const regionFilter = chart.getComponents().filter((co) => co.type === COMPONENT_TYPE.ANNOTATION)[9].component; expect(regionFilter.get('type')).toEqual('regionFilter'); expect(regionFilter.get('shapes')).toHaveLength(1); diff --git a/tests/unit/geometry/base-spec.ts b/tests/unit/geometry/base-spec.ts index f0159096ba..282b0612f7 100644 --- a/tests/unit/geometry/base-spec.ts +++ b/tests/unit/geometry/base-spec.ts @@ -1,12 +1,14 @@ import { flatten } from '@antv/util'; import 'jest-extended'; import { Chart, getEngine } from '../../../src'; +import { GEOMETRY_LIFE_CIRCLE } from '../../../src/constant'; import { getCoordinate } from '../../../src/dependents'; import Geometry from '../../../src/geometry/base'; import * as Shape from '../../../src/geometry/shape/base'; import { LooseObject, ShapeInfo } from '../../../src/interface'; import { getTheme } from '../../../src/theme/'; import { createScaleByField, syncScale } from '../../../src/util/scale'; +import { delay } from '../../util/delay'; import { createCanvas, createDiv, removeDom } from '../../util/dom'; import { createScale, updateScales } from '../../util/scale'; @@ -825,4 +827,66 @@ describe('Geometry', () => { expect(customInfo).toEqual({ hello: 'g2' }); }); + + it('geometry life circle', async () => { + const data = [ + { year: '1991', value: 15468 }, + { year: '1992', value: 16100 }, + { year: '1993', value: 15900 }, + { year: '1998', value: 32040 }, + ]; + + const chart = new Chart({ + container: createDiv(), + width: 500, + height: 400, + }); + + chart.data(data); + const geometry = chart.interval().position('year*valye'); + + const beforFn = jest.fn(); + const afterFn = jest.fn(); + + // 无动画 + geometry.animate(false); + geometry.once(GEOMETRY_LIFE_CIRCLE.BEFORE_DRAW_ANIMATE, () => beforFn(1)); + geometry.once(GEOMETRY_LIFE_CIRCLE.AFTER_DRAW_ANIMATE, () => afterFn(1)); + chart.render(); + + await delay(500); + + expect(beforFn).not.toBeCalled(); + expect(afterFn).not.toBeCalled(); + + // 有动画 + geometry.animate(true); + geometry.once(GEOMETRY_LIFE_CIRCLE.BEFORE_DRAW_ANIMATE, () => beforFn(2)); + geometry.once(GEOMETRY_LIFE_CIRCLE.AFTER_DRAW_ANIMATE, () => afterFn(2)); + chart.changeSize(300, 300); + + await delay(500); + + expect(beforFn).toBeCalledWith(2); + expect(afterFn).toBeCalledWith(2); + + const fn = jest.fn(); + // 设置自定义动画 + geometry.animate({ + update: { + callback: fn, + } + }); + geometry.once(GEOMETRY_LIFE_CIRCLE.BEFORE_DRAW_ANIMATE, () => beforFn(3)); + geometry.once(GEOMETRY_LIFE_CIRCLE.AFTER_DRAW_ANIMATE, () => afterFn(3)); + chart.changeSize(400, 400); + + await delay(500); + + // 自定义的 animate callback 也需要调用 + expect(fn).toBeCalled(); + expect(beforFn).toBeCalledWith(3); + expect(afterFn).toBeCalledWith(3); + expect(fn).toBeCalled(); + }); }); diff --git a/tests/unit/geometry/element/index-spec.ts b/tests/unit/geometry/element/index-spec.ts index 1e7571b14b..e0d0b069af 100644 --- a/tests/unit/geometry/element/index-spec.ts +++ b/tests/unit/geometry/element/index-spec.ts @@ -4,6 +4,7 @@ import Element from '../../../../src/geometry/element'; import * as Shape from '../../../../src/geometry/shape/base'; import '../../../../src/geometry/shape/interval'; import { getTheme } from '../../../../src/theme/'; +import { omit } from '../../../util/omit'; const Rect = getCoordinate('rect'); const G = getEngine('canvas'); @@ -296,7 +297,7 @@ describe('Element', () => { }; // @ts-ignore - expect(element.getAnimateCfg('update')).toEqual({ + expect(omit(element.getAnimateCfg('update'), 'callback')).toEqual({ delay: 1000, }); // @ts-ignore diff --git a/tests/util/omit.ts b/tests/util/omit.ts new file mode 100644 index 0000000000..7dc2aca4ae --- /dev/null +++ b/tests/util/omit.ts @@ -0,0 +1,11 @@ +import { reduce } from '@antv/util'; + +export function omit(obj: any, keys: string[]): object { + // @ts-ignore + return reduce(obj, (r: any, curr: any, key: string) => { + if (!keys.includes(key)) { + r[key] = curr; + } + return r; + }, {}); +}