diff --git a/package-lock.json b/package-lock.json index 179637fa5e..ab83eb9d03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11122,9 +11122,9 @@ } }, "zrender": { - "version": "npm:zrender-nightly@5.3.0-dev.20211126", - "resolved": "https://registry.npmjs.org/zrender-nightly/-/zrender-nightly-5.3.0-dev.20211126.tgz", - "integrity": "sha512-g0ZSkxI4i62QrH7Aa8O8YsRHSopxuxHK1ZH+c91XWzIeQelFfxi8QJjYmzXa/U8iOCXfsEsK8p52uelOcFhXqw==", + "version": "npm:zrender-nightly@5.3.0-dev.20211222", + "resolved": "https://registry.npmjs.org/zrender-nightly/-/zrender-nightly-5.3.0-dev.20211222.tgz", + "integrity": "sha512-pwfuY4PVOfGV/RJ0BE3oJv2PXNvRR8E7HfjbPn6/HQ20DqT+We23fI39EhEFNUWKP5dLHKl+EGb+qpnxQQDVJg==", "requires": { "tslib": "2.3.0" } diff --git a/package.json b/package.json index 0e8d2eb4b4..4b6d2fb866 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ }, "dependencies": { "tslib": "2.3.0", - "zrender": "npm:zrender-nightly@^5.3.0-dev.20211125" + "zrender": "npm:zrender-nightly@^5.3.0-dev.20211222" }, "devDependencies": { "@babel/code-frame": "7.10.4", diff --git a/src/animation/basicTrasition.ts b/src/animation/basicTrasition.ts index bc31f39c48..3863b6b1ae 100644 --- a/src/animation/basicTrasition.ts +++ b/src/animation/basicTrasition.ts @@ -55,7 +55,7 @@ type AnimateOrSetPropsOption = { * Return null if animation is disabled. */ export function getAnimationConfig( - animationType: 'init' | 'update' | 'remove', + animationType: 'enter' | 'update' | 'leave', animatableModel: Model, dataIndex: number, // Extra opts can override the option in animatable model. @@ -124,7 +124,7 @@ export function getAnimationConfig( } function animateOrSetProps( - animationType: 'init' | 'update' | 'remove', + animationType: 'enter' | 'update' | 'leave', el: Element, props: Props, animatableModel?: Model & { @@ -149,11 +149,11 @@ function animateOrSetProps( dataIndex = dataIndex.dataIndex; } - const isRemove = (animationType === 'remove'); + const isRemove = (animationType === 'leave'); if (!isRemove) { // Must stop the remove animation. - el.stopAnimation('remove'); + el.stopAnimation('leave'); } const animationConfig = getAnimationConfig( @@ -245,7 +245,7 @@ export function initProps( cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], during?: AnimateOrSetPropsOption['during'] ) { - animateOrSetProps('init', el, props, animatableModel, dataIndex, cb, during); + animateOrSetProps('enter', el, props, animatableModel, dataIndex, cb, during); } /** @@ -258,7 +258,7 @@ export function initProps( } for (let i = 0; i < el.animators.length; i++) { const animator = el.animators[i]; - if (animator.scope === 'remove') { + if (animator.scope === 'leave') { return true; } } @@ -281,7 +281,7 @@ export function removeElement( return; } - animateOrSetProps('remove', el, props, animatableModel, dataIndex, cb, during); + animateOrSetProps('leave', el, props, animatableModel, dataIndex, cb, during); } function fadeOutDisplayable( diff --git a/src/animation/customGraphicKeyframeAnimation.ts b/src/animation/customGraphicKeyframeAnimation.ts new file mode 100644 index 0000000000..53f9fbbddc --- /dev/null +++ b/src/animation/customGraphicKeyframeAnimation.ts @@ -0,0 +1,162 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { AnimationEasing } from 'zrender/src/animation/easing'; +import Element from 'zrender/src/Element'; +import { keys, filter, each, isArray, indexOf } from 'zrender/src/core/util'; +import { ELEMENT_ANIMATABLE_PROPS } from './customGraphicTransition'; +import { AnimationOption, AnimationOptionMixin, Dictionary } from '../util/types'; +import { Model } from '../echarts.all'; +import { getAnimationConfig } from './basicTrasition'; +import { warn } from '../util/log'; +import { makeInner } from '../util/model'; + +// Helpers for creating keyframe based animations in custom series and graphic components. + +type AnimationKeyframe> = T & { + easing?: AnimationEasing + percent?: number // 0 - 1 +}; + +type StateToRestore = Dictionary; +const getStateToRestore = makeInner(); + +const KEYFRAME_EXCLUDE_KEYS = ['percent', 'easing', 'shape', 'style', 'extra'] as const; + +export interface ElementKeyframeAnimationOption> extends AnimationOption { + // Animation configuration for keyframe based animation. + loop?: boolean + keyframes?: AnimationKeyframe[] +} + +/** + * Stop previous keyframe animation and restore the attributes. + * Avoid new keyframe animation starts with wrong internal state when the percent: 0 is not set. + */ +export function stopPreviousKeyframeAnimationAndRestore(el: Element) { + // Stop previous keyframe animation. + el.stopAnimation('keyframe'); + // Restore + el.attr(getStateToRestore(el)); +} + +export function applyKeyframeAnimation>( + el: Element, + animationOpts: ElementKeyframeAnimationOption | ElementKeyframeAnimationOption[], + animatableModel: Model +) { + if (!animatableModel.isAnimationEnabled() || !animationOpts) { + return; + } + + if (isArray(animationOpts)) { + each(animationOpts, singleAnimationOpts => { + applyKeyframeAnimation(el, singleAnimationOpts, animatableModel); + }); + return; + } + + const keyframes = animationOpts.keyframes; + let duration = animationOpts.duration; + + if (animatableModel && duration == null) { + // Default to use duration of config. + // NOTE: animation config from payload will be ignored because they are mainly for transitions. + const config = getAnimationConfig('enter', animatableModel, 0); + duration = config && config.duration; + } + + if (!keyframes || !duration) { + return; + } + + const stateToRestore: StateToRestore = getStateToRestore(el); + + each(ELEMENT_ANIMATABLE_PROPS, (targetPropName) => { + if (targetPropName && !(el as any)[targetPropName]) { + return; + } + + let animator: ReturnType; + let endFrameIsSet = false; + + // Sort keyframes by percent. + keyframes.sort((a, b) => a.percent - b.percent); + + each(keyframes, kf => { + // Stop current animation. + const animators = el.animators; + const kfValues = targetPropName ? kf[targetPropName] : kf; + + if (__DEV__) { + if (kf.percent >= 1) { + endFrameIsSet = true; + } + } + + if (!kfValues) { + return; + } + + let propKeys = keys(kfValues); + if (!targetPropName) { + // PENDING performance? + propKeys = filter(propKeys, key => indexOf(KEYFRAME_EXCLUDE_KEYS, key) < 0); + } + if (!propKeys.length) { + return; + } + + if (!animator) { + animator = el.animate(targetPropName, animationOpts.loop, true); + animator.scope = 'keyframe'; + } + for (let i = 0; i < animators.length; i++) { + // Stop all other animation that is not keyframe. + if (animators[i] !== animator && animators[i].targetName === animator.targetName) { + animators[i].stopTracks(propKeys); + } + } + + targetPropName && (stateToRestore[targetPropName] = stateToRestore[targetPropName] || {}); + + const savedTarget = targetPropName ? stateToRestore[targetPropName] : stateToRestore; + each(propKeys, key => { + // Save original value. + savedTarget[key] = ((targetPropName ? (el as any)[targetPropName] : el) || {})[key]; + }); + + animator.whenWithKeys(duration * kf.percent, kfValues, propKeys, kf.easing); + }); + if (!animator) { + return; + } + + if (__DEV__) { + if (!endFrameIsSet) { + warn('End frame with percent: 1 is missing in the keyframeAnimation.', true); + } + } + + animator + .delay(animationOpts.delay || 0) + .duration(duration) + .start(animationOpts.easing); + }); +} \ No newline at end of file diff --git a/src/animation/customGraphicTransition.ts b/src/animation/customGraphicTransition.ts new file mode 100644 index 0000000000..e1cdb68dac --- /dev/null +++ b/src/animation/customGraphicTransition.ts @@ -0,0 +1,632 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +// Helpers for creating transitions in custom series and graphic components. +import Element, { ElementAnimateConfig, ElementProps } from 'zrender/src/Element'; + +import { makeInner, normalizeToArray } from '../util/model'; +import { assert, bind, each, eqNaN, extend, hasOwn, indexOf, isArrayLike, keys, reduce } from 'zrender/src/core/util'; +import { cloneValue } from 'zrender/src/animation/Animator'; +import Displayable, { DisplayableProps } from 'zrender/src/graphic/Displayable'; +import Model from '../model/Model'; +import { getAnimationConfig } from './basicTrasition'; +import { Path } from '../util/graphic'; +import { warn } from '../util/log'; +import { AnimationOption, AnimationOptionMixin, ZRStyleProps } from '../util/types'; +import { Dictionary } from 'zrender/src/core/types'; +import { PathStyleProps } from 'zrender/src/graphic/Path'; +import { TRANSFORMABLE_PROPS, TransformProp } from 'zrender/src/core/Transformable'; + +const LEGACY_TRANSFORM_PROPS_MAP = { + position: ['x', 'y'], + scale: ['scaleX', 'scaleY'], + origin: ['originX', 'originY'] +} as const; +const LEGACY_TRANSFORM_PROPS = keys(LEGACY_TRANSFORM_PROPS_MAP); + +const TRANSFORM_PROPS_MAP = reduce(TRANSFORMABLE_PROPS, (obj, key) => { + obj[key] = 1; + return obj; +}, {} as Record); +const transformPropNamesStr = TRANSFORMABLE_PROPS.join(', '); + +// '' means root +export const ELEMENT_ANIMATABLE_PROPS = ['', 'style', 'shape', 'extra'] as const; + +export type TransitionProps = string | string[]; +export type ElementRootTransitionProp = TransformProp | 'shape' | 'extra' | 'style'; + +export interface TransitionOptionMixin> { + transition?: (keyof T & string) | ((keyof T & string)[]) | 'all' + + enterFrom?: T; + leaveTo?: T; + + enterAnimation?: AnimationOption + updateAnimation?: AnimationOption + leaveAnimation?: AnimationOption +}; + +interface LooseElementProps extends ElementProps { + style?: ZRStyleProps; + shape?: Dictionary; +} + +type TransitionElementOption = Partial> & { + shape?: Dictionary & TransitionOptionMixin + style?: PathStyleProps & TransitionOptionMixin + extra?: Dictionary & TransitionOptionMixin + invisible?: boolean + silent?: boolean + autoBatch?: boolean + ignore?: boolean + + during?: (params: TransitionDuringAPI) => void +} & TransitionOptionMixin; + +const transitionInnerStore = makeInner<{ + leaveToProps: ElementProps; + userDuring: (params: TransitionDuringAPI) => void; +}, Element>(); + +export interface TransitionBaseDuringAPI { + // Usually other props do not need to be changed in animation during. + setTransform(key: TransformProp, val: number): this + getTransform(key: TransformProp): number; + setExtra(key: string, val: unknown): this + getExtra(key: string): unknown +} +export interface TransitionDuringAPI< + StyleOpt extends any = any, + ShapeOpt extends any = any +> extends TransitionBaseDuringAPI { + setShape(key: T, val: ShapeOpt[T]): this; + getShape(key: T): ShapeOpt[T]; + setStyle(key: T, val: StyleOpt[T]): this + getStyle(key: T): StyleOpt[T]; +}; + +function getElementAnimationConfig( + animationType: 'enter' | 'update' | 'leave', + el: Element, + elOption: TransitionElementOption, + parentModel: Model, + dataIndex?: number +) { + const animationProp = `${animationType}Animation` as const; + const config: ElementAnimateConfig = getAnimationConfig(animationType, parentModel, dataIndex) || {}; + + const userDuring = transitionInnerStore(el).userDuring; + // Only set when duration is > 0 and it's need to be animated. + if (config.duration > 0) { + // For simplicity, if during not specified, the previous during will not work any more. + config.during = userDuring ? bind(duringCall, { el: el, userDuring: userDuring }) : null; + config.setToFinal = true; + config.scope = animationType; + } + + extend(config, elOption[animationProp]); + return config; +} + + +export function applyUpdateTransition( + el: Element, + elOption: TransitionElementOption, + animatableModel?: Model, + opts?: { + dataIndex?: number, + isInit?: boolean, + clearStyle?: boolean + } +) { + opts = opts || {}; + const {dataIndex, isInit, clearStyle} = opts; + + const hasAnimation = animatableModel.isAnimationEnabled(); + // Save the meta info for further morphing. Like apply on the sub morphing elements. + const store = transitionInnerStore(el); + const styleOpt = elOption.style; + store.userDuring = elOption.during; + + const transFromProps = {} as ElementProps; + const propsToSet = {} as ElementProps; + + prepareTransformAllPropsFinal(el, elOption, propsToSet); + prepareShapeOrExtraAllPropsFinal('shape', elOption, propsToSet); + prepareShapeOrExtraAllPropsFinal('extra', elOption, propsToSet); + + if (!isInit && hasAnimation) { + prepareTransformTransitionFrom(el, elOption, transFromProps); + prepareShapeOrExtraTransitionFrom('shape', el, elOption, transFromProps); + prepareShapeOrExtraTransitionFrom('extra', el, elOption, transFromProps); + prepareStyleTransitionFrom(el, elOption, styleOpt, transFromProps); + } + + (propsToSet as DisplayableProps).style = styleOpt; + + applyPropsDirectly(el, propsToSet, clearStyle); + applyMiscProps(el, elOption); + + if (hasAnimation) { + if (isInit) { + const enterFromProps: ElementProps = {}; + each(ELEMENT_ANIMATABLE_PROPS, propName => { + const prop: TransitionOptionMixin = propName ? elOption[propName] : elOption; + if (prop && prop.enterFrom) { + if (propName) { + (enterFromProps as any)[propName] = (enterFromProps as any)[propName] || {}; + } + extend(propName ? (enterFromProps as any)[propName] : enterFromProps, prop.enterFrom); + } + }); + const config = getElementAnimationConfig('enter', el, elOption, animatableModel, dataIndex); + if (config.duration > 0) { + el.animateFrom(enterFromProps, config); + } + } + else { + applyPropsTransition(el, elOption, dataIndex || 0, animatableModel, transFromProps); + } + } + // Store leave to be used in leave transition. + updateLeaveTo(el, elOption); + + styleOpt ? el.dirty() : el.markRedraw(); +} + +export function updateLeaveTo(el: Element, elOption: TransitionElementOption) { + // Try merge to previous set leaveTo + let leaveToProps: ElementProps = transitionInnerStore(el).leaveToProps; + for (let i = 0; i < ELEMENT_ANIMATABLE_PROPS.length; i++) { + const propName = ELEMENT_ANIMATABLE_PROPS[i]; + const prop: TransitionOptionMixin = propName ? elOption[propName] : elOption; + if (prop && prop.leaveTo) { + if (!leaveToProps) { + leaveToProps = transitionInnerStore(el).leaveToProps = {}; + } + if (propName) { + (leaveToProps as any)[propName] = (leaveToProps as any)[propName] || {}; + } + extend(propName ? (leaveToProps as any)[propName] : leaveToProps, prop.leaveTo); + } + } +} + +export function applyLeaveTransition( + el: Element, + elOption: TransitionElementOption, + animatableModel: Model, + onRemove?: () => void +): void { + if (el) { + const parent = el.parent; + const leaveToProps = transitionInnerStore(el).leaveToProps; + if (leaveToProps) { + // TODO TODO use leave after leaveAnimation in series is introduced + // TODO Data index? + const config = getElementAnimationConfig('update', el, elOption, animatableModel, 0); + config.done = () => { + parent.remove(el); + onRemove && onRemove(); + }; + el.animateTo(leaveToProps, config); + } + else { + parent.remove(el); + onRemove && onRemove(); + } + } +} + +export function isTransitionAll(transition: TransitionProps): transition is 'all' { + return transition === 'all'; +} + + +function applyPropsDirectly( + el: Element, + // Can be null/undefined + allPropsFinal: ElementProps, + clearStyle: boolean +) { + const styleOpt = (allPropsFinal as Displayable).style; + if (!el.isGroup && styleOpt) { + if (clearStyle) { + (el as Displayable).useStyle({}); + + // When style object changed, how to trade the existing animation? + // It is probably complicated and not needed to cover all the cases. + // But still need consider the case: + // (1) When using init animation on `style.opacity`, and before the animation + // ended users triggers an update by mousewhel. At that time the init + // animation should better be continued rather than terminated. + // So after `useStyle` called, we should change the animation target manually + // to continue the effect of the init animation. + // (2) PENDING: If the previous animation targeted at a `val1`, and currently we need + // to update the value to `val2` and no animation declared, should be terminate + // the previous animation or just modify the target of the animation? + // Therotically That will happen not only on `style` but also on `shape` and + // `transfrom` props. But we haven't handle this case at present yet. + // (3) PENDING: Is it proper to visit `animators` and `targetName`? + const animators = el.animators; + for (let i = 0; i < animators.length; i++) { + const animator = animators[i]; + // targetName is the "topKey". + if (animator.targetName === 'style') { + animator.changeTarget((el as Displayable).style); + } + } + } + (el as Displayable).setStyle(styleOpt); + } + + if (allPropsFinal) { + // Not set style here. + (allPropsFinal as DisplayableProps).style = null; + // Set el to the final state firstly. + allPropsFinal && el.attr(allPropsFinal); + (allPropsFinal as DisplayableProps).style = styleOpt; + } +} + +function applyPropsTransition( + el: Element, + elOption: TransitionElementOption, + dataIndex: number, + model: Model, + // Can be null/undefined + transFromProps: ElementProps +): void { + if (transFromProps) { + const config = getElementAnimationConfig('update', el, elOption, model, dataIndex); + if (config.duration > 0) { + el.animateFrom(transFromProps, config); + } + } +} + + +function applyMiscProps( + el: Element, + elOption: TransitionElementOption +) { + // Merge by default. + hasOwn(elOption, 'silent') && (el.silent = elOption.silent); + hasOwn(elOption, 'ignore') && (el.ignore = elOption.ignore); + if (el instanceof Displayable) { + hasOwn(elOption, 'invisible') && ((el as Path).invisible = elOption.invisible); + } + if (el instanceof Path) { + hasOwn(elOption, 'autoBatch') && ((el as Path).autoBatch = elOption.autoBatch); + } +} + +// Use it to avoid it be exposed to user. +const tmpDuringScope = {} as { + el: Element; +}; +const transitionDuringAPI: TransitionDuringAPI = { + // Usually other props do not need to be changed in animation during. + setTransform(key: TransformProp, val: unknown) { + if (__DEV__) { + assert(hasOwn(TRANSFORM_PROPS_MAP, key), 'Only ' + transformPropNamesStr + ' available in `setTransform`.'); + } + tmpDuringScope.el[key] = val as number; + return this; + }, + getTransform(key: TransformProp): number { + if (__DEV__) { + assert(hasOwn(TRANSFORM_PROPS_MAP, key), 'Only ' + transformPropNamesStr + ' available in `getTransform`.'); + } + return tmpDuringScope.el[key]; + }, + setShape(key: any, val: unknown) { + if (__DEV__) { + assertNotReserved(key); + } + const el = tmpDuringScope.el as Path; + const shape = el.shape || (el.shape = {}); + shape[key] = val; + el.dirtyShape && el.dirtyShape(); + return this; + }, + getShape(key: any): any { + if (__DEV__) { + assertNotReserved(key); + } + const shape = (tmpDuringScope.el as Path).shape; + if (shape) { + return shape[key]; + } + }, + setStyle(key: any, val: unknown) { + if (__DEV__) { + assertNotReserved(key); + } + const el = tmpDuringScope.el as Displayable; + const style = el.style; + if (style) { + if (__DEV__) { + if (eqNaN(val)) { + warn('style.' + key + ' must not be assigned with NaN.'); + } + } + style[key] = val; + el.dirtyStyle && el.dirtyStyle(); + } + return this; + }, + getStyle(key: any): any { + if (__DEV__) { + assertNotReserved(key); + } + const style = (tmpDuringScope.el as Displayable).style; + if (style) { + return style[key]; + } + }, + setExtra(key: any, val: unknown) { + if (__DEV__) { + assertNotReserved(key); + } + const extra = (tmpDuringScope.el as LooseElementProps).extra + || ((tmpDuringScope.el as LooseElementProps).extra = {}); + extra[key] = val; + return this; + }, + getExtra(key: string): unknown { + if (__DEV__) { + assertNotReserved(key); + } + const extra = (tmpDuringScope.el as LooseElementProps).extra; + if (extra) { + return extra[key]; + } + } +}; + +function assertNotReserved(key: string) { + if (__DEV__) { + if (key === 'transition' || key === 'enterFrom' || key === 'leaveTo') { + throw new Error('key must not be "' + key + '"'); + } + } +} + +function duringCall( + this: { + el: Element; + userDuring: (params: TransitionDuringAPI) => void; + } +): void { + // Do not provide "percent" until some requirements come. + // Because consider thies case: + // enterFrom: {x: 100, y: 30}, transition: 'x'. + // And enter duration is different from update duration. + // Thus it might be confused about the meaning of "percent" in during callback. + const scope = this; + const el = scope.el; + if (!el) { + return; + } + // If el is remove from zr by reason like legend, during still need to called, + // becuase el will be added back to zr and the prop value should not be incorrect. + + const latestUserDuring = transitionInnerStore(el).userDuring; + const scopeUserDuring = scope.userDuring; + // Ensured a during is only called once in each animation frame. + // If a during is called multiple times in one frame, maybe some users' calulation logic + // might be wrong (not sure whether this usage exists). + // The case of a during might be called twice can be: by default there is a animator for + // 'x', 'y' when init. Before the init animation finished, call `setOption` to start + // another animators for 'style'/'shape'/'extra'. + if (latestUserDuring !== scopeUserDuring) { + // release + scope.el = scope.userDuring = null; + return; + } + + tmpDuringScope.el = el; + + // Give no `this` to user in "during" calling. + scopeUserDuring(transitionDuringAPI); + + // FIXME: if in future meet the case that some prop will be both modified in `during` and `state`, + // consider the issue that the prop might be incorrect when return to "normal" state. +} + +function prepareShapeOrExtraTransitionFrom( + mainAttr: 'shape' | 'extra', + fromEl: Element, + elOption: TransitionOptionMixin, + transFromProps: LooseElementProps +): void { + + const attrOpt: Dictionary & TransitionOptionMixin = (elOption as any)[mainAttr]; + if (!attrOpt) { + return; + } + + const elPropsInAttr = (fromEl as LooseElementProps)[mainAttr]; + let transFromPropsInAttr: Dictionary; + + if (elPropsInAttr) { + const transition = elOption.transition; + const attrTransition = attrOpt.transition; + if (attrTransition) { + !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); + if (isTransitionAll(attrTransition)) { + extend(transFromPropsInAttr, elPropsInAttr); + } + else { + const transitionKeys = normalizeToArray(attrTransition); + for (let i = 0; i < transitionKeys.length; i++) { + const key = transitionKeys[i]; + const elVal = elPropsInAttr[key]; + transFromPropsInAttr[key] = elVal; + } + } + } + else if (isTransitionAll(transition) || indexOf(transition, mainAttr) >= 0) { + !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); + const elPropsInAttrKeys = keys(elPropsInAttr); + for (let i = 0; i < elPropsInAttrKeys.length; i++) { + const key = elPropsInAttrKeys[i]; + const elVal = elPropsInAttr[key]; + if (isNonStyleTransitionEnabled((attrOpt as any)[key], elVal)) { + transFromPropsInAttr[key] = elVal; + } + } + } + } +} + +function prepareShapeOrExtraAllPropsFinal( + mainAttr: 'shape' | 'extra', + elOption: TransitionElementOption, + allProps: LooseElementProps +): void { + const attrOpt: Dictionary = (elOption as any)[mainAttr]; + if (!attrOpt) { + return; + } + const allPropsInAttr = allProps[mainAttr] = {} as Dictionary; + const keysInAttr = keys(attrOpt); + for (let i = 0; i < keysInAttr.length; i++) { + const key = keysInAttr[i]; + // To avoid share one object with different element, and + // to avoid user modify the object inexpectedly, have to clone. + allPropsInAttr[key] = cloneValue((attrOpt as any)[key]); + } +} + +function prepareTransformTransitionFrom( + el: Element, + elOption: TransitionElementOption, + transFromProps: ElementProps +): void { + const transition = elOption.transition; + const transitionKeys = isTransitionAll(transition) + ? TRANSFORMABLE_PROPS + : normalizeToArray(transition || []); + for (let i = 0; i < transitionKeys.length; i++) { + const key = transitionKeys[i]; + if (key === 'style' || key === 'shape' || key === 'extra') { + continue; + } + const elVal = (el as any)[key]; + if (__DEV__) { + checkTransformPropRefer(key, 'el.transition'); + } + // Do not clone, animator will perform that clone. + (transFromProps as any)[key] = elVal; + } +} + +function prepareTransformAllPropsFinal( + el: Element, + elOption: TransitionElementOption, + allProps: ElementProps +): void { + for (let i = 0; i < LEGACY_TRANSFORM_PROPS.length; i++) { + const legacyName = LEGACY_TRANSFORM_PROPS[i]; + const xyName = LEGACY_TRANSFORM_PROPS_MAP[legacyName]; + const legacyArr = (elOption as any)[legacyName]; + if (legacyArr) { + allProps[xyName[0]] = legacyArr[0]; + allProps[xyName[1]] = legacyArr[1]; + } + } + + for (let i = 0; i < TRANSFORMABLE_PROPS.length; i++) { + const key = TRANSFORMABLE_PROPS[i]; + if (elOption[key] != null) { + allProps[key] = elOption[key]; + } + } +} + +function prepareStyleTransitionFrom( + fromEl: Element, + elOption: TransitionElementOption, + styleOpt: TransitionElementOption['style'], + transFromProps: LooseElementProps +): void { + if (!styleOpt) { + return; + } + + const fromElStyle = (fromEl as LooseElementProps).style as LooseElementProps['style']; + let transFromStyleProps: LooseElementProps['style']; + + if (fromElStyle) { + const styleTransition = styleOpt.transition; + const elTransition = elOption.transition; + if (styleTransition && !isTransitionAll(styleTransition)) { + const transitionKeys = normalizeToArray(styleTransition); + !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); + for (let i = 0; i < transitionKeys.length; i++) { + const key = transitionKeys[i]; + const elVal = (fromElStyle as any)[key]; + // Do not clone, see `checkNonStyleTansitionRefer`. + (transFromStyleProps as any)[key] = elVal; + } + } + else if ( + (fromEl as Displayable).getAnimationStyleProps + && ( + isTransitionAll(elTransition) + || isTransitionAll(styleTransition) + || indexOf(elTransition, 'style') >= 0 + ) + ) { + const animationProps = (fromEl as Displayable).getAnimationStyleProps(); + const animationStyleProps = animationProps ? animationProps.style : null; + if (animationStyleProps) { + !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); + const styleKeys = keys(styleOpt); + for (let i = 0; i < styleKeys.length; i++) { + const key = styleKeys[i]; + if ((animationStyleProps as Dictionary)[key]) { + const elVal = (fromElStyle as any)[key]; + (transFromStyleProps as any)[key] = elVal; + } + } + } + } + } +} + +function isNonStyleTransitionEnabled(optVal: unknown, elVal: unknown): boolean { + // The same as `checkNonStyleTansitionRefer`. + return !isArrayLike(optVal) + ? (optVal != null && isFinite(optVal as number)) + : optVal !== elVal; +} + +let checkTransformPropRefer: (key: string, usedIn: string) => void; +if (__DEV__) { + checkTransformPropRefer = function (key: string, usedIn: string): void { + if (!hasOwn(TRANSFORM_PROPS_MAP, key)) { + warn('Prop `' + key + '` is not a permitted in `' + usedIn + '`. ' + + 'Only `' + keys(TRANSFORM_PROPS_MAP).join('`, `') + '` are permitted.'); + } + }; +} \ No newline at end of file diff --git a/src/chart/custom/CustomSeries.ts b/src/chart/custom/CustomSeries.ts index ae4e548400..6477df2817 100644 --- a/src/chart/custom/CustomSeries.ts +++ b/src/chart/custom/CustomSeries.ts @@ -18,11 +18,12 @@ */ import Displayable from 'zrender/src/graphic/Displayable'; -import { ImageStyleProps } from 'zrender/src/graphic/Image'; +import { ImageProps, ImageStyleProps } from 'zrender/src/graphic/Image'; import { PathProps, PathStyleProps } from 'zrender/src/graphic/Path'; import { ZRenderType } from 'zrender/src/zrender'; import { BarGridLayoutOptionForCustomSeries, BarGridLayoutResult } from '../../layout/barGrid'; import { + AnimationOption, BlurScope, CallbackDataParams, Dictionary, @@ -43,7 +44,7 @@ import { TextCommonOption, ZRStyleProps } from '../../util/types'; -import Element, { ElementProps } from 'zrender/src/Element'; +import Element from 'zrender/src/Element'; import SeriesData, { DefaultDataVisual } from '../../data/SeriesData'; import GlobalModel from '../../model/Global'; import createSeriesData from '../helper/createSeriesData'; @@ -63,25 +64,17 @@ import { Ring, Sector } from '../../util/graphic'; -import { TextStyleProps } from 'zrender/src/graphic/Text'; - - -export interface LooseElementProps extends ElementProps { - style?: ZRStyleProps; - shape?: Dictionary; -} +import { TextProps, TextStyleProps } from 'zrender/src/graphic/Text'; +import { GroupProps } from 'zrender/src/graphic/Group'; +import { + TransitionOptionMixin, + TransitionBaseDuringAPI, + TransitionDuringAPI +} from '../../animation/customGraphicTransition'; +import { TransformProp } from 'zrender/src/core/Transformable'; +import { ElementKeyframeAnimationOption } from '../../animation/customGraphicKeyframeAnimation'; export type CustomExtraElementInfo = Dictionary; -export const TRANSFORM_PROPS = { - x: 1, - y: 1, - scaleX: 1, - scaleY: 1, - originX: 1, - originY: 1, - rotation: 1 -} as const; -export type TransformProp = keyof typeof TRANSFORM_PROPS; // Also compat with ec4, where // `visual('color') visual('borderColor')` is supported. @@ -102,19 +95,7 @@ export const NON_STYLE_VISUAL_PROPS = { } as const; export type NonStyleVisualProps = keyof typeof NON_STYLE_VISUAL_PROPS; -// Do not declare "Dictionary" in TransitionAnyOption to restrict the type check. -export type TransitionAnyOption = { - transition?: TransitionAnyProps; - enterFrom?: Dictionary; - leaveTo?: Dictionary; -}; -type TransitionAnyProps = string | string[]; -type TransitionTransformOption = { - transition?: ElementRootTransitionProp | ElementRootTransitionProp[]; - enterFrom?: Dictionary; - leaveTo?: Dictionary; -}; -type ElementRootTransitionProp = TransformProp | 'shape' | 'extra' | 'style'; +// Do not declare "Dictionary" in ElementTransitionOptions to restrict the type check. type ShapeMorphingOption = { /** * If do shape morphing animation when type is changed. @@ -123,27 +104,9 @@ type ShapeMorphingOption = { morph?: boolean }; -export interface CustomBaseDuringAPI { - // Usually other props do not need to be changed in animation during. - setTransform(key: TransformProp, val: number): this - getTransform(key: TransformProp): number; - setExtra(key: string, val: unknown): this - getExtra(key: string): unknown -} -export interface CustomDuringAPI< - StyleOpt extends any = any, - ShapeOpt extends any = any -> extends CustomBaseDuringAPI { - setShape(key: T, val: ShapeOpt[T]): this; - getShape(key: T): ShapeOpt[T]; - setStyle(key: T, val: StyleOpt[T]): this - getStyle(key: T): StyleOpt[T]; -}; - - export interface CustomBaseElementOption extends Partial>, TransitionTransformOption { +>> { // element type, required. type: string; id?: string; @@ -155,16 +118,20 @@ export interface CustomBaseElementOption extends Partial & TransitionAnyOption; + extra?: Dictionary & TransitionOptionMixin; // updateDuringAnimation - during?(params: CustomBaseDuringAPI): void; + during?(params: TransitionBaseDuringAPI): void; + + enterAnimation?: AnimationOption + updateAnimation?: AnimationOption + leaveAnimation?: AnimationOption }; export interface CustomDisplayableOption extends CustomBaseElementOption, Partial> { - style?: ZRStyleProps & TransitionAnyOption; - during?(params: CustomDuringAPI): void; + style?: ZRStyleProps; + during?(params: TransitionDuringAPI): void; /** * @deprecated */ @@ -178,12 +145,9 @@ export interface CustomDisplayableOptionOnState extends Partial> { // `false` means remove emphasis trigger. - style?: (ZRStyleProps & TransitionAnyOption) | false; - - - during?(params: CustomDuringAPI): void; + style?: ZRStyleProps | false; } -export interface CustomGroupOption extends CustomBaseElementOption { +export interface CustomGroupOption extends CustomBaseElementOption, TransitionOptionMixin{ type: 'group'; width?: number; height?: number; @@ -191,13 +155,18 @@ export interface CustomGroupOption extends CustomBaseElementOption { diffChildrenByName?: boolean; children: CustomElementOption[]; $mergeChildren?: false | 'byName' | 'byIndex'; + + keyframeAnimation?: ElementKeyframeAnimationOption | ElementKeyframeAnimationOption[] } export interface CustomBaseZRPathOption - extends CustomDisplayableOption, ShapeMorphingOption { + extends CustomDisplayableOption, ShapeMorphingOption, TransitionOptionMixin { autoBatch?: boolean; - shape?: T & TransitionAnyOption; - style?: PathProps['style'] - during?(params: CustomDuringAPI): void; + shape?: T & TransitionOptionMixin; + style?: PathProps['style'] & TransitionOptionMixin + during?(params: TransitionDuringAPI): void; + + keyframeAnimation?: ElementKeyframeAnimationOption + | ElementKeyframeAnimationOption[] } interface BuiltinShapes { @@ -240,25 +209,29 @@ export type CustomPathOption = CreateCustomBuitinPathOption | CustomSVGPathOption; export interface CustomImageOptionOnState extends CustomDisplayableOptionOnState { - style?: ImageStyleProps & TransitionAnyOption; + style?: ImageStyleProps; } -export interface CustomImageOption extends CustomDisplayableOption { +export interface CustomImageOption extends CustomDisplayableOption, TransitionOptionMixin { type: 'image'; - style?: ImageStyleProps & TransitionAnyOption; + style?: ImageStyleProps & TransitionOptionMixin; emphasis?: CustomImageOptionOnState; blur?: CustomImageOptionOnState; select?: CustomImageOptionOnState; + + keyframeAnimation?: ElementKeyframeAnimationOption | ElementKeyframeAnimationOption[] } export interface CustomTextOptionOnState extends CustomDisplayableOptionOnState { - style?: TextStyleProps & TransitionAnyOption; + style?: TextStyleProps; } -export interface CustomTextOption extends CustomDisplayableOption { +export interface CustomTextOption extends CustomDisplayableOption, TransitionOptionMixin { type: 'text'; - style?: TextStyleProps & TransitionAnyOption; + style?: TextStyleProps & TransitionOptionMixin; emphasis?: CustomTextOptionOnState; blur?: CustomTextOptionOnState; select?: CustomTextOptionOnState; + + keyframeAnimation?: ElementKeyframeAnimationOption | ElementKeyframeAnimationOption[] } export type CustomElementOption = CustomPathOption @@ -394,9 +367,7 @@ export const customInnerStore = makeInner<{ customImagePath: CustomImageOption['style']['image']; // customText: string; txConZ2Set: number; - leaveToProps: ElementProps; option: CustomElementOption; - userDuring: CustomBaseElementOption['during']; }, Element>(); export default class CustomSeriesModel extends SeriesModel { diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index 91854af208..0d6b5a351b 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -18,8 +18,7 @@ */ import { - hasOwn, assert, isString, retrieve2, retrieve3, defaults, each, - keys, bind, eqNaN, indexOf + hasOwn, assert, isString, retrieve2, retrieve3, defaults, each, indexOf } from 'zrender/src/core/util'; import * as graphicUtil from '../../util/graphic'; import { setDefaultStateProxy, enableHoverEmphasis } from '../../util/states'; @@ -45,7 +44,7 @@ import { OrdinalRawValue, InnerDecalObject } from '../../util/types'; -import Element, { ElementProps, ElementTextConfig } from 'zrender/src/Element'; +import Element, { ElementTextConfig } from 'zrender/src/Element'; import prepareCartesian2d from '../../coord/cartesian/prepareCustom'; import prepareGeo from '../../coord/geo/prepareCustom'; import prepareSingleAxis from '../../coord/single/prepareCustom'; @@ -54,7 +53,7 @@ import prepareCalendar from '../../coord/calendar/prepareCustom'; import SeriesData, { DefaultDataVisual } from '../../data/SeriesData'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; -import Displayable, { DisplayableProps } from 'zrender/src/graphic/Displayable'; +import Displayable from 'zrender/src/graphic/Displayable'; import Axis2D from '../../coord/cartesian/Axis2D'; import { RectLike } from 'zrender/src/core/BoundingRect'; import { PathStyleProps } from 'zrender/src/graphic/Path'; @@ -67,14 +66,10 @@ import { warnDeprecated } from '../../util/styleCompat'; import { ItemStyleProps } from '../../model/mixin/itemStyle'; -import { warn, throwError } from '../../util/log'; +import { throwError } from '../../util/log'; import { createOrUpdatePatternFromDecal } from '../../util/decal'; import CustomSeriesModel, { - CustomDuringAPI, - TransformProp, - TRANSFORM_PROPS, CustomImageOption, - CustomBaseElementOption, CustomElementOption, CustomElementOptionOnState, CustomSVGPathOption, @@ -89,22 +84,21 @@ import CustomSeriesModel, { STYLE_VISUAL_TYPE, NON_STYLE_VISUAL_PROPS, customInnerStore, - LooseElementProps, PrepareCustomInfo, CustomPathOption, - CustomRootElementOption + CustomRootElementOption, + CustomSeriesOption } from './CustomSeries'; -import { - prepareShapeOrExtraAllPropsFinal, - prepareShapeOrExtraTransitionFrom, - prepareStyleTransitionFrom, - prepareTransformAllPropsFinal, - prepareTransformTransitionFrom -} from './prepare'; import { PatternObject } from 'zrender/src/graphic/Pattern'; -import { CustomSeriesOption } from '../../export/option'; - -const transformPropNamesStr = keys(TRANSFORM_PROPS).join(', '); +import { + applyLeaveTransition, + applyUpdateTransition, + ElementRootTransitionProp +} from '../../animation/customGraphicTransition'; +import { + applyKeyframeAnimation, + stopPreviousKeyframeAnimationAndRestore +} from '../../animation/customGraphicKeyframeAnimation'; const EMPHASIS = 'emphasis' as const; const NORMAL = 'normal' as const; @@ -123,6 +117,7 @@ const PATH_LABEL = { blur: [BLUR, 'label'], select: [SELECT, 'label'] } as const; +const DEFAULT_TRANSITION: ElementRootTransitionProp[] = ['x', 'y']; // Use prefix to avoid index to be the same as el.name, // which will cause weird update animation. const GROUP_DIFF_PREFIX = 'e\0\0'; @@ -232,7 +227,8 @@ export default class CustomChartView extends ChartView { ); }) .remove(function (oldIdx) { - doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group); + const el = oldData.getItemGraphicEl(oldIdx); + applyLeaveTransition(el, customInnerStore(el).option, customSeries); }) .update(function (newIdx, oldIdx) { const oldEl = oldData.getItemGraphicEl(oldIdx); @@ -446,10 +442,12 @@ function updateElNormal( elOption: CustomElementOption, attachedTxInfo: AttachedTxInfo, seriesModel: CustomSeriesModel, - isInit: boolean, - isTextContent: boolean + isInit: boolean ): void { + // Stop and restore before update any other attributes. + stopPreviousKeyframeAnimationAndRestore(el); + const txCfgOpt = attachedTxInfo && attachedTxInfo.normal.cfg; if (txCfgOpt) { // PENDING: whether use user object directly rather than clone? @@ -457,6 +455,11 @@ function updateElNormal( el.setTextConfig(txCfgOpt); } + // Default transition ['x', 'y'] + if (elOption && elOption.transition == null) { + elOption.transition = DEFAULT_TRANSITION; + } + // Do some normalization on style. const styleOpt = elOption && (elOption as CustomDisplayableOption).style; @@ -482,274 +485,22 @@ function updateElNormal( (styleOpt as InnerCustomZRPathOptionStyle).__decalPattern = decalPattern; } - // Save the meta info for further morphing. Like apply on the sub morphing elements. - const store = customInnerStore(el); - store.userDuring = elOption.during; - - const transFromProps = {} as ElementProps; - const propsToSet = {} as ElementProps; - - prepareShapeOrExtraTransitionFrom('shape', el, elOption, transFromProps, isInit); - prepareShapeOrExtraAllPropsFinal('shape', elOption, propsToSet); - prepareTransformTransitionFrom(el, elOption, transFromProps, isInit); - prepareTransformAllPropsFinal(el, elOption, propsToSet); - prepareShapeOrExtraTransitionFrom('extra', el, elOption, transFromProps, isInit); - prepareShapeOrExtraAllPropsFinal('extra', elOption, propsToSet); - prepareStyleTransitionFrom(el, elOption, styleOpt, transFromProps, isInit); - (propsToSet as DisplayableProps).style = styleOpt; - applyPropsDirectly(el, propsToSet); - applyPropsTransition(el, dataIndex, seriesModel, transFromProps, isInit); - applyMiscProps(el, elOption, isTextContent); - - styleOpt ? el.dirty() : el.markRedraw(); -} - -function applyMiscProps( - el: Element, elOption: CustomElementOption, isTextContent: boolean -) { - // Merge by default. - hasOwn(elOption, 'silent') && (el.silent = elOption.silent); - hasOwn(elOption, 'ignore') && (el.ignore = elOption.ignore); if (isDisplayable(el)) { - hasOwn(elOption, 'invisible') && (el.invisible = (elOption as CustomDisplayableOption).invisible); - } - if (isPath(el)) { - hasOwn(elOption, 'autoBatch') && (el.autoBatch = (elOption as CustomBaseZRPathOption).autoBatch); - } - - if (!isTextContent) { - // `elOption.info` enables user to mount some info on - // elements and use them in event handlers. - // Update them only when user specified, otherwise, remain. - hasOwn(elOption, 'info') && (customInnerStore(el).info = elOption.info); - } -} - -function applyPropsDirectly( - el: Element, - // Can be null/undefined - allPropsFinal: ElementProps -) { - const elDisplayable = el.isGroup ? null : el as Displayable; - const styleOpt = (allPropsFinal as Displayable).style; - - if (elDisplayable && styleOpt) { - - // PENDING: here the input style object is used directly. - // Good for performance but bad for compatibility control. - elDisplayable.useStyle(styleOpt); - - const decalPattern = (styleOpt as InnerCustomZRPathOptionStyle).__decalPattern; - if (decalPattern) { - elDisplayable.style.decal = decalPattern; - } - - // When style object changed, how to trade the existing animation? - // It is probably complicated and not needed to cover all the cases. - // But still need consider the case: - // (1) When using init animation on `style.opacity`, and before the animation - // ended users triggers an update by mousewhel. At that time the init - // animation should better be continued rather than terminated. - // So after `useStyle` called, we should change the animation target manually - // to continue the effect of the init animation. - // (2) PENDING: If the previous animation targeted at a `val1`, and currently we need - // to update the value to `val2` and no animation declared, should be terminate - // the previous animation or just modify the target of the animation? - // Therotically That will happen not only on `style` but also on `shape` and - // `transfrom` props. But we haven't handle this case at present yet. - // (3) PENDING: Is it proper to visit `animators` and `targetName`? - const animators = elDisplayable.animators; - for (let i = 0; i < animators.length; i++) { - const animator = animators[i]; - // targetName is the "topKey". - if (animator.targetName === 'style') { - animator.changeTarget(elDisplayable.style); - } - } - } - - if (allPropsFinal) { - // Not set style here. - (allPropsFinal as DisplayableProps).style = null; - // Set el to the final state firstly. - allPropsFinal && el.attr(allPropsFinal); - (allPropsFinal as DisplayableProps).style = styleOpt; - } -} - -function applyPropsTransition( - el: Element, - dataIndex: number, - seriesModel: CustomSeriesModel, - // Can be null/undefined - transFromProps: ElementProps, - isInit: boolean -): void { - if (transFromProps) { - // NOTE: Do not use `el.updateDuringAnimation` here becuase `el.updateDuringAnimation` will - // be called mutiple time in each animation frame. For example, if both "transform" props - // and shape props and style props changed, it will generate three animator and called - // one-by-one in each animation frame. - // We use the during in `animateTo/From` params. - const userDuring = customInnerStore(el).userDuring; - // For simplicity, if during not specified, the previous during will not work any more. - const cfgDuringCall = userDuring ? bind(duringCall, { el: el, userDuring: userDuring }) : null; - const cfg = { - dataIndex: dataIndex, - isFrom: true, - during: cfgDuringCall - }; - isInit - ? graphicUtil.initProps(el, transFromProps, seriesModel, cfg) - : graphicUtil.updateProps(el, transFromProps, seriesModel, cfg); - } -} - - -// Use it to avoid it be exposed to user. -const tmpDuringScope = {} as { - el: Element; - isShapeDirty: boolean; - isStyleDirty: boolean; -}; -const customDuringAPI: CustomDuringAPI = { - // Usually other props do not need to be changed in animation during. - setTransform(key: TransformProp, val: unknown) { - if (__DEV__) { - assert(hasOwn(TRANSFORM_PROPS, key), 'Only ' + transformPropNamesStr + ' available in `setTransform`.'); - } - tmpDuringScope.el[key] = val as number; - return this; - }, - getTransform(key: TransformProp): number { - if (__DEV__) { - assert(hasOwn(TRANSFORM_PROPS, key), 'Only ' + transformPropNamesStr + ' available in `getTransform`.'); - } - return tmpDuringScope.el[key]; - }, - setShape(key: any, val: unknown) { - if (__DEV__) { - assertNotReserved(key); - } - const shape = (tmpDuringScope.el as graphicUtil.Path).shape - || ((tmpDuringScope.el as graphicUtil.Path).shape = {}); - shape[key] = val; - tmpDuringScope.isShapeDirty = true; - return this; - }, - getShape(key: any): any { - if (__DEV__) { - assertNotReserved(key); - } - const shape = (tmpDuringScope.el as graphicUtil.Path).shape; - if (shape) { - return shape[key]; - } - }, - setStyle(key: any, val: unknown) { - if (__DEV__) { - assertNotReserved(key); - } - const style = (tmpDuringScope.el as Displayable).style; - if (style) { - if (__DEV__) { - if (eqNaN(val)) { - warn('style.' + key + ' must not be assigned with NaN.'); - } + if (styleOpt) { + const decalPattern = (styleOpt as InnerCustomZRPathOptionStyle).__decalPattern; + if (decalPattern) { + (styleOpt as PathStyleProps).decal = decalPattern; } - style[key] = val; - tmpDuringScope.isStyleDirty = true; - } - return this; - }, - getStyle(key: any): any { - if (__DEV__) { - assertNotReserved(key); - } - const style = (tmpDuringScope.el as Displayable).style; - if (style) { - return style[key]; - } - }, - setExtra(key: any, val: unknown) { - if (__DEV__) { - assertNotReserved(key); - } - const extra = (tmpDuringScope.el as LooseElementProps).extra - || ((tmpDuringScope.el as LooseElementProps).extra = {}); - extra[key] = val; - return this; - }, - getExtra(key: string): unknown { - if (__DEV__) { - assertNotReserved(key); - } - const extra = (tmpDuringScope.el as LooseElementProps).extra; - if (extra) { - return extra[key]; } } -}; -function assertNotReserved(key: string) { - if (__DEV__) { - if (key === 'transition' || key === 'enterFrom' || key === 'leaveTo') { - throw new Error('key must not be "' + key + '"'); - } - } -} - -function duringCall( - this: { - el: Element; - userDuring: CustomBaseElementOption['during'] - } -): void { - // Do not provide "percent" until some requirements come. - // Because consider thies case: - // enterFrom: {x: 100, y: 30}, transition: 'x'. - // And enter duration is different from update duration. - // Thus it might be confused about the meaning of "percent" in during callback. - const scope = this; - const el = scope.el; - if (!el) { - return; - } - // If el is remove from zr by reason like legend, during still need to called, - // becuase el will be added back to zr and the prop value should not be incorrect. - - const latestUserDuring = customInnerStore(el).userDuring; - const scopeUserDuring = scope.userDuring; - // Ensured a during is only called once in each animation frame. - // If a during is called multiple times in one frame, maybe some users' calulation logic - // might be wrong (not sure whether this usage exists). - // The case of a during might be called twice can be: by default there is a animator for - // 'x', 'y' when init. Before the init animation finished, call `setOption` to start - // another animators for 'style'/'shape'/'extra'. - if (latestUserDuring !== scopeUserDuring) { - // release - scope.el = scope.userDuring = null; - return; - } - - tmpDuringScope.el = el; - tmpDuringScope.isShapeDirty = false; - tmpDuringScope.isStyleDirty = false; - - // Give no `this` to user in "during" calling. - scopeUserDuring(customDuringAPI); - - if (tmpDuringScope.isShapeDirty && (el as graphicUtil.Path).dirtyShape) { - (el as graphicUtil.Path).dirtyShape(); - } - if (tmpDuringScope.isStyleDirty && (el as Displayable).dirtyStyle) { - (el as Displayable).dirtyStyle(); - } - // markRedraw() will be called by default in during. - // FIXME `this.markRedraw();` directly ? + applyUpdateTransition(el, elOption, seriesModel, { + dataIndex, + isInit, + clearStyle: true + }); - // FIXME: if in future meet the case that some prop will be both modified in `during` and `state`, - // consider the issue that the prop might be incorrect when return to "normal" state. + applyKeyframeAnimation(el, elOption.keyframeAnimation, seriesModel); } function updateElOnState( @@ -757,9 +508,7 @@ function updateElOnState( el: Element, elStateOpt: CustomElementOptionOnState, styleOpt: CustomElementOptionOnState['style'], - attachedTxInfo: AttachedTxInfo, - isRoot: boolean, - isTextContent: boolean + attachedTxInfo: AttachedTxInfo ): void { const elDisplayable = el.isGroup ? null : el as Displayable; const txCfgOpt = attachedTxInfo && attachedTxInfo[state].cfg; @@ -1180,7 +929,7 @@ function createOrUpdateItem( group.remove(existsEl); return; } - const el = doCreateOrUpdateEl(api, existsEl, dataIndex, elOption, seriesModel, group, true); + const el = doCreateOrUpdateEl(api, existsEl, dataIndex, elOption, seriesModel, group); el && data.setItemGraphicEl(dataIndex, el); el && enableHoverEmphasis(el, elOption.focus, elOption.blurScope); @@ -1194,8 +943,7 @@ function doCreateOrUpdateEl( dataIndex: number, elOption: CustomElementOption, seriesModel: CustomSeriesModel, - group: ViewRootGroup, - isRoot: boolean + group: ViewRootGroup ): Element { if (__DEV__) { @@ -1266,16 +1014,19 @@ function doCreateOrUpdateEl( elOption, attachedTxInfoTmp, seriesModel, - isInit, - false + isInit ); + // `elOption.info` enables user to mount some info on + // elements and use them in event handlers. + // Update them only when user specified, otherwise, remain. + hasOwn(elOption, 'info') && (customInnerStore(el).info = elOption.info); for (let i = 0; i < STATES.length; i++) { const stateName = STATES[i]; if (stateName !== NORMAL) { const otherStateOpt = retrieveStateOption(elOption, stateName); const otherStyleOpt = retrieveStyleOptionOnState(elOption, otherStateOpt, stateName); - updateElOnState(stateName, el, otherStateOpt, otherStyleOpt, attachedTxInfoTmp, isRoot, false); + updateElOnState(stateName, el, otherStateOpt, otherStyleOpt, attachedTxInfoTmp); } } @@ -1312,8 +1063,8 @@ function doesElNeedRecreate(el: Element, elOption: CustomElementOption, seriesMo && elOptionType !== elInner.customGraphicType ) || (elOptionType === 'path' - && hasOwnPathData(elOptionShape) - && getPathData(elOptionShape) !== elInner.customPathData + && hasOwnPathData(elOptionShape as CustomSVGPathOption['shape']) + && getPathData(elOptionShape as CustomSVGPathOption['shape']) !== elInner.customPathData ) || (elOptionType === 'image' && hasOwn(elOptionStyle, 'image') @@ -1363,7 +1114,7 @@ function doCreateOrUpdateClipPath( el.setClipPath(clipPath); } updateElNormal( - null, clipPath, dataIndex, clipPathOpt, null, seriesModel, isInit, false + null, clipPath, dataIndex, clipPathOpt, null, seriesModel, isInit ); } // If not define `clipPath` in option, do nothing unnecessary. @@ -1414,9 +1165,7 @@ function doCreateOrUpdateAttachedTx( textContent.clearStates(); } - updateElNormal( - null, textContent, dataIndex, txConOptNormal, null, seriesModel, isInit, true - ); + updateElNormal(null, textContent, dataIndex, txConOptNormal, null, seriesModel, isInit); const txConStlOptNormal = txConOptNormal && (txConOptNormal as CustomDisplayableOption).style; for (let i = 0; i < STATES.length; i++) { const stateName = STATES[i]; @@ -1427,7 +1176,7 @@ function doCreateOrUpdateAttachedTx( textContent, txConOptOtherState, retrieveStyleOptionOnState(txConOptNormal, txConOptOtherState, stateName), - null, false, true + null ); } } @@ -1568,15 +1317,15 @@ function mergeChildren( dataIndex, newChildren[index] as CustomElementOption, seriesModel, - el, - false + el ); } for (let i = el.childCount() - 1; i >= index; i--) { // Do not supprot leave elements that are not mentioned in the latest // `renderItem` return. Otherwise users may not have a clear and simple // concept that how to contorl all of the elements. - doRemoveEl(el.childAt(i), seriesModel, el); + const child = el.childAt(i); + applyLeaveTransition(child, customInnerStore(el).option, seriesModel); } } @@ -1622,32 +1371,14 @@ function processAddUpdate( context.dataIndex, childOption, context.seriesModel, - context.group, - false + context.group ); } function processRemove(this: DataDiffer, oldIndex: number): void { const context = this.context; const child = context.oldChildren[oldIndex]; - doRemoveEl(child, context.seriesModel, context.group); -} - -function doRemoveEl( - el: Element, - seriesModel: CustomSeriesModel, - group: ViewRootGroup -): void { - if (el) { - const leaveToProps = customInnerStore(el).leaveToProps; - leaveToProps - ? graphicUtil.updateProps(el, leaveToProps, seriesModel, { - cb: function () { - group.remove(el); - } - }) - : group.remove(el); - } + applyLeaveTransition(child, customInnerStore(child).option, context.seriesModel); } /** diff --git a/src/chart/custom/prepare.ts b/src/chart/custom/prepare.ts deleted file mode 100644 index 37f8d99e30..0000000000 --- a/src/chart/custom/prepare.ts +++ /dev/null @@ -1,353 +0,0 @@ -/* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ - -import Transformable from 'zrender/src/core/Transformable'; -import Element, { ElementProps } from 'zrender/src/Element'; -import { Dictionary } from '../../util/types'; -import { - CustomDisplayableOption, - CustomElementOption, - customInnerStore, - LooseElementProps, - TransformProp, - TRANSFORM_PROPS, - TransitionAnyOption -} from './CustomSeries'; -import { normalizeToArray } from '../../util/model'; -import { assert, hasOwn, indexOf, isArrayLike, keys } from 'zrender/src/core/util'; -import { cloneValue } from 'zrender/src/animation/Animator'; -import Displayable from 'zrender/src/graphic/Displayable'; - -const LEGACY_TRANSFORM_PROPS = { - position: ['x', 'y'], - scale: ['scaleX', 'scaleY'], - origin: ['originX', 'originY'] -} as const; -type LegacyTransformProp = keyof typeof LEGACY_TRANSFORM_PROPS; - -function setLegacyTransformProp( - elOption: CustomElementOption, - targetProps: Partial>, - legacyName: LegacyTransformProp -): void { - const legacyArr = (elOption as any)[legacyName]; - const xyName = LEGACY_TRANSFORM_PROPS[legacyName]; - if (legacyArr) { - targetProps[xyName[0]] = legacyArr[0]; - targetProps[xyName[1]] = legacyArr[1]; - } -} - -function setTransformProp( - elOption: CustomElementOption, - allProps: Partial>, - name: TransformProp -): void { - if (elOption[name] != null) { - allProps[name] = elOption[name]; - } -} - -function setTransformPropToTransitionFrom( - transitionFrom: Partial>, - name: TransformProp, - fromTransformable?: Transformable // If provided, retrieve from the element. -): void { - if (fromTransformable) { - transitionFrom[name] = fromTransformable[name]; - } -} - - -// See [STRATEGY_TRANSITION] -export function prepareShapeOrExtraTransitionFrom( - mainAttr: 'shape' | 'extra', - fromEl: Element, - elOption: CustomElementOption, - transFromProps: LooseElementProps, - isInit: boolean -): void { - - const attrOpt: Dictionary & TransitionAnyOption = (elOption as any)[mainAttr]; - if (!attrOpt) { - return; - } - - const elPropsInAttr = (fromEl as LooseElementProps)[mainAttr]; - let transFromPropsInAttr: Dictionary; - - const enterFrom = attrOpt.enterFrom; - if (isInit && enterFrom) { - !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); - const enterFromKeys = keys(enterFrom); - for (let i = 0; i < enterFromKeys.length; i++) { - // `enterFrom` props are not necessarily also declared in `shape`/`style`/..., - // for example, `opacity` can only declared in `enterFrom` but not in `style`. - const key = enterFromKeys[i]; - // Do not clone, animator will perform that clone. - transFromPropsInAttr[key] = enterFrom[key]; - } - } - - if (!isInit && elPropsInAttr) { - if (attrOpt.transition) { - !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); - const transitionKeys = normalizeToArray(attrOpt.transition); - for (let i = 0; i < transitionKeys.length; i++) { - const key = transitionKeys[i]; - const elVal = elPropsInAttr[key]; - if (__DEV__) { - checkNonStyleTansitionRefer(key, (attrOpt as any)[key], elVal); - } - // Do not clone, see `checkNonStyleTansitionRefer`. - transFromPropsInAttr[key] = elVal; - } - } - else if (indexOf(elOption.transition, mainAttr) >= 0) { - !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); - const elPropsInAttrKeys = keys(elPropsInAttr); - for (let i = 0; i < elPropsInAttrKeys.length; i++) { - const key = elPropsInAttrKeys[i]; - const elVal = elPropsInAttr[key]; - if (isNonStyleTransitionEnabled((attrOpt as any)[key], elVal)) { - transFromPropsInAttr[key] = elVal; - } - } - } - } - - const leaveTo = attrOpt.leaveTo; - if (leaveTo) { - const leaveToProps = getOrCreateLeaveToPropsFromEl(fromEl); - const leaveToPropsInAttr: Dictionary = leaveToProps[mainAttr] || (leaveToProps[mainAttr] = {}); - const leaveToKeys = keys(leaveTo); - for (let i = 0; i < leaveToKeys.length; i++) { - const key = leaveToKeys[i]; - leaveToPropsInAttr[key] = leaveTo[key]; - } - } -} - -export function prepareShapeOrExtraAllPropsFinal( - mainAttr: 'shape' | 'extra', - elOption: CustomElementOption, - allProps: LooseElementProps -): void { - const attrOpt: Dictionary & TransitionAnyOption = (elOption as any)[mainAttr]; - if (!attrOpt) { - return; - } - const allPropsInAttr = allProps[mainAttr] = {} as Dictionary; - const keysInAttr = keys(attrOpt); - for (let i = 0; i < keysInAttr.length; i++) { - const key = keysInAttr[i]; - // To avoid share one object with different element, and - // to avoid user modify the object inexpectedly, have to clone. - allPropsInAttr[key] = cloneValue((attrOpt as any)[key]); - } -} - -// See [STRATEGY_TRANSITION]. -export function prepareTransformTransitionFrom( - el: Element, - elOption: CustomElementOption, - transFromProps: ElementProps, - isInit: boolean -): void { - const enterFrom = elOption.enterFrom; - if (isInit && enterFrom) { - const enterFromKeys = keys(enterFrom); - for (let i = 0; i < enterFromKeys.length; i++) { - const key = enterFromKeys[i] as TransformProp; - if (__DEV__) { - checkTransformPropRefer(key, 'el.enterFrom'); - } - // Do not clone, animator will perform that clone. - transFromProps[key] = enterFrom[key] as number; - } - } - - if (!isInit) { - if (elOption.transition) { - const transitionKeys = normalizeToArray(elOption.transition); - for (let i = 0; i < transitionKeys.length; i++) { - const key = transitionKeys[i]; - if (key === 'style' || key === 'shape' || key === 'extra') { - continue; - } - const elVal = el[key]; - if (__DEV__) { - checkTransformPropRefer(key, 'el.transition'); - checkNonStyleTansitionRefer(key, elOption[key], elVal); - } - // Do not clone, see `checkNonStyleTansitionRefer`. - transFromProps[key] = elVal; - } - } - // This default transition see [STRATEGY_TRANSITION] - else { - setTransformPropToTransitionFrom(transFromProps, 'x', el); - setTransformPropToTransitionFrom(transFromProps, 'y', el); - } - } - - const leaveTo = elOption.leaveTo; - if (leaveTo) { - const leaveToProps = getOrCreateLeaveToPropsFromEl(el); - const leaveToKeys = keys(leaveTo); - for (let i = 0; i < leaveToKeys.length; i++) { - const key = leaveToKeys[i] as TransformProp; - if (__DEV__) { - checkTransformPropRefer(key, 'el.leaveTo'); - } - leaveToProps[key] = leaveTo[key] as number; - } - } -} - -export function prepareTransformAllPropsFinal( - el: Element, - elOption: CustomElementOption, - allProps: ElementProps -): void { - setLegacyTransformProp(elOption, allProps, 'position'); - setLegacyTransformProp(elOption, allProps, 'scale'); - setLegacyTransformProp(elOption, allProps, 'origin'); - - setTransformProp(elOption, allProps, 'x'); - setTransformProp(elOption, allProps, 'y'); - setTransformProp(elOption, allProps, 'scaleX'); - setTransformProp(elOption, allProps, 'scaleY'); - setTransformProp(elOption, allProps, 'originX'); - setTransformProp(elOption, allProps, 'originY'); - setTransformProp(elOption, allProps, 'rotation'); -} - -// See [STRATEGY_TRANSITION]. -export function prepareStyleTransitionFrom( - fromEl: Element, - elOption: CustomElementOption, - styleOpt: CustomDisplayableOption['style'], - transFromProps: LooseElementProps, - isInit: boolean -): void { - if (!styleOpt) { - return; - } - - const fromElStyle = (fromEl as LooseElementProps).style as LooseElementProps['style']; - let transFromStyleProps: LooseElementProps['style']; - - const enterFrom = styleOpt.enterFrom; - if (isInit && enterFrom) { - const enterFromKeys = keys(enterFrom); - !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); - for (let i = 0; i < enterFromKeys.length; i++) { - const key = enterFromKeys[i]; - // Do not clone, animator will perform that clone. - (transFromStyleProps as any)[key] = enterFrom[key]; - } - } - - if (!isInit && fromElStyle) { - if (styleOpt.transition) { - const transitionKeys = normalizeToArray(styleOpt.transition); - !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); - for (let i = 0; i < transitionKeys.length; i++) { - const key = transitionKeys[i]; - const elVal = (fromElStyle as any)[key]; - // Do not clone, see `checkNonStyleTansitionRefer`. - (transFromStyleProps as any)[key] = elVal; - } - } - else if ( - (fromEl as Displayable).getAnimationStyleProps - && indexOf(elOption.transition, 'style') >= 0 - ) { - const animationProps = (fromEl as Displayable).getAnimationStyleProps(); - const animationStyleProps = animationProps ? animationProps.style : null; - if (animationStyleProps) { - !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); - const styleKeys = keys(styleOpt); - for (let i = 0; i < styleKeys.length; i++) { - const key = styleKeys[i]; - if ((animationStyleProps as Dictionary)[key]) { - const elVal = (fromElStyle as any)[key]; - (transFromStyleProps as any)[key] = elVal; - } - } - } - } - } - - const leaveTo = styleOpt.leaveTo; - if (leaveTo) { - const leaveToKeys = keys(leaveTo); - const leaveToProps = getOrCreateLeaveToPropsFromEl(fromEl); - const leaveToStyleProps = leaveToProps.style || (leaveToProps.style = {}); - for (let i = 0; i < leaveToKeys.length; i++) { - const key = leaveToKeys[i]; - (leaveToStyleProps as any)[key] = leaveTo[key]; - } - } -} - -let checkNonStyleTansitionRefer: (propName: string, optVal: unknown, elVal: unknown) => void; -if (__DEV__) { - checkNonStyleTansitionRefer = function (propName: string, optVal: unknown, elVal: unknown): void { - if (!isArrayLike(optVal)) { - assert( - optVal != null && isFinite(optVal as number), - 'Prop `' + propName + '` must refer to a finite number or ArrayLike for transition.' - ); - } - else { - // Try not to copy array for performance, but if user use the same object in different - // call of `renderItem`, it will casue animation transition fail. - assert( - optVal !== elVal, - 'Prop `' + propName + '` must use different Array object each time for transition.' - ); - } - }; -} - -function isNonStyleTransitionEnabled(optVal: unknown, elVal: unknown): boolean { - // The same as `checkNonStyleTansitionRefer`. - return !isArrayLike(optVal) - ? (optVal != null && isFinite(optVal as number)) - : optVal !== elVal; -} - -let checkTransformPropRefer: (key: string, usedIn: string) => void; -if (__DEV__) { - checkTransformPropRefer = function (key: string, usedIn: string): void { - assert( - hasOwn(TRANSFORM_PROPS, key), - 'Prop `' + key + '` is not a permitted in `' + usedIn + '`. ' - + 'Only `' + keys(TRANSFORM_PROPS).join('`, `') + '` are permitted.' - ); - }; -} - -function getOrCreateLeaveToPropsFromEl(el: Element): LooseElementProps { - const innerEl = customInnerStore(el); - return innerEl.leaveToProps || (innerEl.leaveToProps = {}); -} - diff --git a/src/chart/gauge/GaugeView.ts b/src/chart/gauge/GaugeView.ts index d81e10ba59..46173183db 100644 --- a/src/chart/gauge/GaugeView.ts +++ b/src/chart/gauge/GaugeView.ts @@ -421,11 +421,15 @@ class GaugeView extends ChartView { if (showProgress || showPointer) { data.diff(oldData) .add(function (idx) { + const val = data.get(valueDim, idx) as number; if (showPointer) { const pointer = createPointer(idx, startAngle); + // TODO hide pointer on NaN value? graphic.initProps(pointer, { - rotation: -(linearMap(data.get(valueDim, idx) as number, valueExtent, angleExtent, true) - + Math.PI / 2) + rotation: -( + (isNaN(+val) ? angleExtent[0] : linearMap(val, valueExtent, angleExtent, true)) + + Math.PI / 2 + ) }, seriesModel); group.add(pointer); data.setItemGraphicEl(idx, pointer); @@ -436,7 +440,7 @@ class GaugeView extends ChartView { const isClip = progressModel.get('clip'); graphic.initProps(progress, { shape: { - endAngle: linearMap(data.get(valueDim, idx) as number, valueExtent, angleExtent, isClip) + endAngle: linearMap(val, valueExtent, angleExtent, isClip) } }, seriesModel); group.add(progress); @@ -447,6 +451,7 @@ class GaugeView extends ChartView { } }) .update(function (newIdx, oldIdx) { + const val = data.get(valueDim, newIdx) as number; if (showPointer) { const previousPointer = oldData.getItemGraphicEl(oldIdx) as PointerPath; const previousRotate = previousPointer ? previousPointer.rotation : startAngle; @@ -454,7 +459,7 @@ class GaugeView extends ChartView { pointer.rotation = previousRotate; graphic.updateProps(pointer, { rotation: -( - linearMap(data.get(valueDim, newIdx) as number, valueExtent, angleExtent, true) + (isNaN(+val) ? angleExtent[0] : linearMap(val, valueExtent, angleExtent, true)) + Math.PI / 2 ) }, seriesModel); @@ -469,9 +474,7 @@ class GaugeView extends ChartView { const isClip = progressModel.get('clip'); graphic.updateProps(progress, { shape: { - endAngle: linearMap( - data.get(valueDim, newIdx) as number, valueExtent, angleExtent, isClip - ) + endAngle: linearMap(val, valueExtent, angleExtent, isClip) } }, seriesModel); group.add(progress); diff --git a/src/chart/helper/Symbol.ts b/src/chart/helper/Symbol.ts index be2a5a4a2e..8f994eb6b8 100644 --- a/src/chart/helper/Symbol.ts +++ b/src/chart/helper/Symbol.ts @@ -198,8 +198,8 @@ class Symbol extends graphic.Group { } if (disableAnimation) { - // Must stop remove animation manually if don't call initProps or updateProps. - this.childAt(0).stopAnimation('remove'); + // Must stop leave transition manually if don't call initProps or updateProps. + this.childAt(0).stopAnimation('leave'); } this._seriesModel = seriesModel; diff --git a/src/component/graphic/GraphicModel.ts b/src/component/graphic/GraphicModel.ts new file mode 100644 index 0000000000..a6ab11ff51 --- /dev/null +++ b/src/component/graphic/GraphicModel.ts @@ -0,0 +1,454 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import * as zrUtil from 'zrender/src/core/util'; +import * as modelUtil from '../../util/model'; +import { + ComponentOption, + BoxLayoutOptionMixin, + Dictionary, + ZRStyleProps, + OptionId, + CommonTooltipOption, + AnimationOptionMixin, + AnimationOption +} from '../../util/types'; +import ComponentModel from '../../model/Component'; +import Element, { ElementTextConfig } from 'zrender/src/Element'; +import Displayable from 'zrender/src/graphic/Displayable'; +import { PathProps, PathStyleProps } from 'zrender/src/graphic/Path'; +import { ImageStyleProps, ImageProps } from 'zrender/src/graphic/Image'; +import { TextStyleProps, TextProps } from 'zrender/src/graphic/Text'; +import GlobalModel from '../../model/Global'; +import { copyLayoutParams, mergeLayoutParam } from '../../util/layout'; +import { TransitionOptionMixin } from '../../animation/customGraphicTransition'; +import { ElementKeyframeAnimationOption } from '../../animation/customGraphicKeyframeAnimation'; +import { GroupProps } from 'zrender/src/graphic/Group'; +import { TransformProp } from 'zrender/src/core/Transformable'; +import { ElementEventNameWithOn } from 'zrender/src/core/types'; + +interface GraphicComponentBaseElementOption extends + Partial>, + /** + * left/right/top/bottom: (like 12, '22%', 'center', default undefined) + * If left/rigth is set, shape.x/shape.cx/position will not be used. + * If top/bottom is set, shape.y/shape.cy/position will not be used. + * This mechanism is useful when you want to position a group/element + * against the right side or the center of this container. + */ + Partial> { + + /** + * element type, mandatory. + * Only can be omit if call setOption not at the first time and perform merge. + */ + type?: string; + + id?: OptionId; + name?: string; + + // Only internal usage. Use specified value does NOT make sense. + parentId?: OptionId; + parentOption?: GraphicComponentElementOption; + children?: GraphicComponentElementOption[]; + hv?: [boolean, boolean]; + + /** + * bounding: (enum: 'all' (default) | 'raw') + * Specify how to calculate boundingRect when locating. + * 'all': Get uioned and transformed boundingRect + * from both itself and its descendants. + * This mode simplies confining a group of elements in the bounding + * of their ancester container (e.g., using 'right: 0'). + * 'raw': Only use the boundingRect of itself and before transformed. + * This mode is similar to css behavior, which is useful when you + * want an element to be able to overflow its container. (Consider + * a rotated circle needs to be located in a corner.) + */ + bounding?: 'raw' | 'all'; + + /** + * info: custom info. enables user to mount some info on elements and use them + * in event handlers. Update them only when user specified, otherwise, remain. + */ + info?: GraphicExtraElementInfo; + + + // `false` means remove the clipPath + clipPath?: Omit | false; + + textContent?: Omit; + textConfig?: ElementTextConfig; + + $action?: 'merge' | 'replace' | 'remove'; + + tooltip?: CommonTooltipOption; + + enterAnimation?: AnimationOption + updateAnimation?: AnimationOption + leaveAnimation?: AnimationOption +}; + + +export interface GraphicComponentDisplayableOption extends + GraphicComponentBaseElementOption, + Partial> { + + style?: ZRStyleProps + z2?: number +} +// TODO: states? +// interface GraphicComponentDisplayableOptionOnState extends Partial> { +// style?: ZRStyleProps; +// } +export interface GraphicComponentGroupOption + extends GraphicComponentBaseElementOption, TransitionOptionMixin { + type?: 'group'; + + /** + * width/height: (can only be pixel value, default 0) + * Only be used to specify contianer(group) size, if needed. And + * can not be percentage value (like '33%'). See the reason in the + * layout algorithm below. + */ + width?: number; + height?: number; + + // TODO: Can only set focus, blur on the root element. + // children: Omit[]; + children: GraphicComponentElementOption[]; + + keyframeAnimation?: ElementKeyframeAnimationOption | ElementKeyframeAnimationOption[] +}; +export interface GraphicComponentZRPathOption + extends GraphicComponentDisplayableOption, TransitionOptionMixin { + shape?: PathProps['shape'] & TransitionOptionMixin; + style?: PathStyleProps & TransitionOptionMixin + + keyframeAnimation?: ElementKeyframeAnimationOption | ElementKeyframeAnimationOption[]; +} +export interface GraphicComponentImageOption + extends GraphicComponentDisplayableOption, TransitionOptionMixin { + type?: 'image'; + style?: ImageStyleProps & TransitionOptionMixin; + + keyframeAnimation?: ElementKeyframeAnimationOption | ElementKeyframeAnimationOption[]; +} +// TODO: states? +// interface GraphicComponentImageOptionOnState extends GraphicComponentDisplayableOptionOnState { +// style?: ImageStyleProps; +// } +interface GraphicComponentTextOption + extends Omit, TransitionOptionMixin { + type?: 'text'; + style?: TextStyleProps & TransitionOptionMixin; + + keyframeAnimation?: ElementKeyframeAnimationOption | ElementKeyframeAnimationOption[]; +} +export type GraphicComponentElementOption = + GraphicComponentGroupOption | + GraphicComponentZRPathOption | + GraphicComponentImageOption | + GraphicComponentTextOption; +// type GraphicComponentElementOptionOnState = +// GraphicComponentDisplayableOptionOnState +// | GraphicComponentImageOptionOnState; +type GraphicExtraElementInfo = Dictionary; +export type ElementMap = zrUtil.HashMap; + + +export type GraphicComponentLooseOption = (GraphicComponentOption | GraphicComponentElementOption) & { + mainType?: 'graphic'; +}; + +export interface GraphicComponentOption extends ComponentOption, AnimationOptionMixin { + // Note: elements is always behind its ancestors in this elements array. + elements?: GraphicComponentElementOption[]; +}; + +export function setKeyInfoToNewElOption( + resultItem: ReturnType[number], + newElOption: GraphicComponentElementOption +): void { + const existElOption = resultItem.existing as GraphicComponentElementOption; + + // Set id and type after id assigned. + newElOption.id = resultItem.keyInfo.id; + !newElOption.type && existElOption && (newElOption.type = existElOption.type); + + // Set parent id if not specified + if (newElOption.parentId == null) { + const newElParentOption = newElOption.parentOption; + if (newElParentOption) { + newElOption.parentId = newElParentOption.id; + } + else if (existElOption) { + newElOption.parentId = existElOption.parentId; + } + } + + // Clear + newElOption.parentOption = null; +} + +function isSetLoc( + obj: GraphicComponentElementOption, + props: ('left' | 'right' | 'top' | 'bottom')[] +): boolean { + let isSet; + zrUtil.each(props, function (prop) { + obj[prop] != null && obj[prop] !== 'auto' && (isSet = true); + }); + return isSet; +} +function mergeNewElOptionToExist( + existList: GraphicComponentElementOption[], + index: number, + newElOption: GraphicComponentElementOption +): void { + // Update existing options, for `getOption` feature. + const newElOptCopy = zrUtil.extend({}, newElOption); + const existElOption = existList[index]; + + const $action = newElOption.$action || 'merge'; + if ($action === 'merge') { + if (existElOption) { + if (__DEV__) { + const newType = newElOption.type; + zrUtil.assert( + !newType || existElOption.type === newType, + 'Please set $action: "replace" to change `type`' + ); + } + + // We can ensure that newElOptCopy and existElOption are not + // the same object, so `merge` will not change newElOptCopy. + zrUtil.merge(existElOption, newElOptCopy, true); + // Rigid body, use ignoreSize. + mergeLayoutParam(existElOption, newElOptCopy, { ignoreSize: true }); + // Will be used in render. + copyLayoutParams(newElOption, existElOption); + + // Copy transition info to new option so it can be used in the transition. + // DO IT AFTER merge + copyTransitionInfo(newElOption, existElOption); + copyTransitionInfo(newElOption, existElOption, 'shape'); + copyTransitionInfo(newElOption, existElOption, 'style'); + copyTransitionInfo(newElOption, existElOption, 'extra'); + + // Copy clipPath + newElOption.clipPath = existElOption.clipPath; + } + else { + existList[index] = newElOptCopy; + } + } + else if ($action === 'replace') { + existList[index] = newElOptCopy; + } + else if ($action === 'remove') { + // null will be cleaned later. + existElOption && (existList[index] = null); + } +} + +const TRANSITION_PROPS_TO_COPY = ['transition', 'enterFrom', 'leaveTo']; +const ROOT_TRANSITION_PROPS_TO_COPY = + TRANSITION_PROPS_TO_COPY.concat(['enterAnimation', 'updateAnimation', 'leaveAnimation']); +function copyTransitionInfo( + target: GraphicComponentElementOption, + source: GraphicComponentElementOption, + targetProp?: string +) { + if (targetProp) { + if (!(target as any)[targetProp] + && (source as any)[targetProp] + ) { + // TODO avoid creating this empty object when there is no transition configuration. + (target as any)[targetProp] = {}; + } + target = (target as any)[targetProp]; + source = (source as any)[targetProp]; + } + if (!target || !source) { + return; + } + + const props = targetProp ? TRANSITION_PROPS_TO_COPY : ROOT_TRANSITION_PROPS_TO_COPY; + for (let i = 0; i < props.length; i++) { + const prop = props[i]; + if ((target as any)[prop] == null && (source as any)[prop] != null) { + (target as any)[prop] = (source as any)[prop]; + } + } +} + +function setLayoutInfoToExist( + existItem: GraphicComponentElementOption, + newElOption: GraphicComponentElementOption +) { + if (!existItem) { + return; + } + existItem.hv = newElOption.hv = [ + // Rigid body, dont care `width`. + isSetLoc(newElOption, ['left', 'right']), + // Rigid body, dont care `height`. + isSetLoc(newElOption, ['top', 'bottom']) + ]; + // Give default group size. Otherwise layout error may occur. + if (existItem.type === 'group') { + const existingGroupOpt = existItem as GraphicComponentGroupOption; + const newGroupOpt = newElOption as GraphicComponentGroupOption; + existingGroupOpt.width == null && (existingGroupOpt.width = newGroupOpt.width = 0); + existingGroupOpt.height == null && (existingGroupOpt.height = newGroupOpt.height = 0); + } +} + +export class GraphicComponentModel extends ComponentModel { + + static type = 'graphic'; + type = GraphicComponentModel.type; + + preventAutoZ = true; + + static defaultOption: GraphicComponentOption = { + elements: [] + // parentId: null + }; + + /** + * Save el options for the sake of the performance (only update modified graphics). + * The order is the same as those in option. (ancesters -> descendants) + */ + private _elOptionsToUpdate: GraphicComponentElementOption[]; + + mergeOption(option: GraphicComponentOption, ecModel: GlobalModel): void { + // Prevent default merge to elements + const elements = this.option.elements; + this.option.elements = null; + + super.mergeOption(option, ecModel); + + this.option.elements = elements; + } + + optionUpdated(newOption: GraphicComponentOption, isInit: boolean): void { + const thisOption = this.option; + const newList = (isInit ? thisOption : newOption).elements; + const existList = thisOption.elements = isInit ? [] : thisOption.elements; + + const flattenedList = [] as GraphicComponentElementOption[]; + this._flatten(newList, flattenedList, null); + + const mappingResult = modelUtil.mappingToExists(existList, flattenedList, 'normalMerge'); + + // Clear elOptionsToUpdate + const elOptionsToUpdate = this._elOptionsToUpdate = [] as GraphicComponentElementOption[]; + + zrUtil.each(mappingResult, function (resultItem, index) { + const newElOption = resultItem.newOption as GraphicComponentElementOption; + + if (__DEV__) { + zrUtil.assert( + zrUtil.isObject(newElOption) || resultItem.existing, + 'Empty graphic option definition' + ); + } + + if (!newElOption) { + return; + } + + elOptionsToUpdate.push(newElOption); + + setKeyInfoToNewElOption(resultItem, newElOption); + + mergeNewElOptionToExist(existList, index, newElOption); + + setLayoutInfoToExist(existList[index], newElOption); + + }, this); + + // Clean + thisOption.elements = zrUtil.filter(existList, (item) => { + // $action should be volatile, otherwise option gotten from + // `getOption` will contain unexpected $action. + item && delete item.$action; + return item != null; + }); + } + + /** + * Convert + * [{ + * type: 'group', + * id: 'xx', + * children: [{type: 'circle'}, {type: 'polygon'}] + * }] + * to + * [ + * {type: 'group', id: 'xx'}, + * {type: 'circle', parentId: 'xx'}, + * {type: 'polygon', parentId: 'xx'} + * ] + */ + private _flatten( + optionList: GraphicComponentElementOption[], + result: GraphicComponentElementOption[], + parentOption: GraphicComponentElementOption + ): void { + zrUtil.each(optionList, function (option) { + if (!option) { + return; + } + + if (parentOption) { + option.parentOption = parentOption; + } + + result.push(option); + + const children = option.children; + if (option.type === 'group' && children) { + this._flatten(children, result, option); + } + // Deleting for JSON output, and for not affecting group creation. + delete option.children; + }, this); + } + + // FIXME + // Pass to view using payload? setOption has a payload? + useElOptionsToUpdate(): GraphicComponentElementOption[] { + const els = this._elOptionsToUpdate; + // Clear to avoid render duplicately when zooming. + this._elOptionsToUpdate = null; + return els; + } +} diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts new file mode 100644 index 0000000000..eeebfd188e --- /dev/null +++ b/src/component/graphic/GraphicView.ts @@ -0,0 +1,502 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import * as zrUtil from 'zrender/src/core/util'; +import { TextStyleProps } from 'zrender/src/graphic/Text'; +import Displayable from 'zrender/src/graphic/Displayable'; +import Element from 'zrender/src/Element'; +import * as modelUtil from '../../util/model'; +import * as graphicUtil from '../../util/graphic'; +import * as layoutUtil from '../../util/layout'; +import { parsePercent } from '../../util/number'; +import GlobalModel from '../../model/Global'; +import ComponentView from '../../view/Component'; +import ExtensionAPI from '../../core/ExtensionAPI'; +import { getECData } from '../../util/innerStore'; +import { isEC4CompatibleStyle, convertFromEC4CompatibleStyle } from '../../util/styleCompat'; +import { + ElementMap, + GraphicComponentModel, + GraphicComponentDisplayableOption, + GraphicComponentZRPathOption, + GraphicComponentGroupOption, + GraphicComponentElementOption +} from './GraphicModel'; +import { + applyLeaveTransition, + applyUpdateTransition, + isTransitionAll, + updateLeaveTo +} from '../../animation/customGraphicTransition'; +import { updateProps } from '../../animation/basicTrasition'; +import { + applyKeyframeAnimation, + stopPreviousKeyframeAnimationAndRestore +} from '../../animation/customGraphicKeyframeAnimation'; + +const nonShapeGraphicElements = { + // Reserved but not supported in graphic component. + path: null as unknown, + compoundPath: null as unknown, + + // Supported in graphic component. + group: graphicUtil.Group, + image: graphicUtil.Image, + text: graphicUtil.Text +} as const; +type NonShapeGraphicElementType = keyof typeof nonShapeGraphicElements; + +export const inner = modelUtil.makeInner<{ + width: number; + height: number; + isNew: boolean; + id: string; + type: string; + option: GraphicComponentElementOption +}, Element>(); +// ------------------------ +// View +// ------------------------ +export class GraphicComponentView extends ComponentView { + + static type = 'graphic'; + type = GraphicComponentView.type; + + private _elMap: ElementMap; + private _lastGraphicModel: GraphicComponentModel; + + init() { + this._elMap = zrUtil.createHashMap(); + } + + render(graphicModel: GraphicComponentModel, ecModel: GlobalModel, api: ExtensionAPI): void { + // Having leveraged between use cases and algorithm complexity, a very + // simple layout mechanism is used: + // The size(width/height) can be determined by itself or its parent (not + // implemented yet), but can not by its children. (Top-down travel) + // The location(x/y) can be determined by the bounding rect of itself + // (can including its descendants or not) and the size of its parent. + // (Bottom-up travel) + + // When `chart.clear()` or `chart.setOption({...}, true)` with the same id, + // view will be reused. + if (graphicModel !== this._lastGraphicModel) { + this._clear(); + } + this._lastGraphicModel = graphicModel; + + this._updateElements(graphicModel); + this._relocate(graphicModel, api); + } + + /** + * Update graphic elements. + */ + private _updateElements(graphicModel: GraphicComponentModel): void { + const elOptionsToUpdate = graphicModel.useElOptionsToUpdate(); + + if (!elOptionsToUpdate) { + return; + } + + const elMap = this._elMap; + const rootGroup = this.group; + + const globalZ = graphicModel.get('z'); + const globalZLevel = graphicModel.get('zlevel'); + + // Top-down tranverse to assign graphic settings to each elements. + zrUtil.each(elOptionsToUpdate, function (elOption) { + const id = modelUtil.convertOptionIdName(elOption.id, null); + const elExisting = id != null ? elMap.get(id) : null; + const parentId = modelUtil.convertOptionIdName(elOption.parentId, null); + const targetElParent = (parentId != null ? elMap.get(parentId) : rootGroup) as graphicUtil.Group; + + const elType = elOption.type; + const elOptionStyle = (elOption as GraphicComponentDisplayableOption).style; + if (elType === 'text' && elOptionStyle) { + // In top/bottom mode, textVerticalAlign should not be used, which cause + // inaccurately locating. + if (elOption.hv && elOption.hv[1]) { + (elOptionStyle as any).textVerticalAlign = + (elOptionStyle as any).textBaseline = + (elOptionStyle as TextStyleProps).verticalAlign = + (elOptionStyle as TextStyleProps).align = null; + } + } + + let textContentOption = (elOption as GraphicComponentZRPathOption).textContent; + let textConfig = (elOption as GraphicComponentZRPathOption).textConfig; + if (elOptionStyle + && isEC4CompatibleStyle(elOptionStyle, elType, !!textConfig, !!textContentOption)) { + const convertResult = + convertFromEC4CompatibleStyle(elOptionStyle, elType, true) as GraphicComponentZRPathOption; + if (!textConfig && convertResult.textConfig) { + textConfig = (elOption as GraphicComponentZRPathOption).textConfig = convertResult.textConfig; + } + if (!textContentOption && convertResult.textContent) { + textContentOption = convertResult.textContent; + } + } + + // Remove unnecessary props to avoid potential problems. + const elOptionCleaned = getCleanedElOption(elOption); + + + // For simple, do not support parent change, otherwise reorder is needed. + if (__DEV__) { + elExisting && zrUtil.assert( + targetElParent === elExisting.parent, + 'Changing parent is not supported.' + ); + } + + const $action = elOption.$action || 'merge'; + const isMerge = $action === 'merge'; + const isReplace = $action === 'replace'; + if (isMerge) { + const isInit = !elExisting; + let el = elExisting; + if (isInit) { + el = createEl(id, targetElParent, elOption.type, elMap); + } + else { + el && (inner(el).isNew = false); + // Stop and restore before update any other attributes. + stopPreviousKeyframeAnimationAndRestore(el); + } + if (el) { + applyUpdateTransition( + el, + elOptionCleaned, + graphicModel, + { isInit } + ); + updateCommonAttrs(el, elOption, globalZ, globalZLevel); + } + } + else if (isReplace) { + removeEl(elExisting, elOption, elMap, graphicModel); + const el = createEl(id, targetElParent, elOption.type, elMap); + if (el) { + applyUpdateTransition( + el, + elOptionCleaned, + graphicModel, + { isInit: true} + ); + updateCommonAttrs(el, elOption, globalZ, globalZLevel); + } + } + else if ($action === 'remove') { + updateLeaveTo(elExisting, elOption); + removeEl(elExisting, elOption, elMap, graphicModel); + } + + const el = elMap.get(id); + + if (el && textContentOption) { + if (isMerge) { + const textContentExisting = el.getTextContent(); + textContentExisting + ? textContentExisting.attr(textContentOption) + : el.setTextContent(new graphicUtil.Text(textContentOption)); + } + else if (isReplace) { + el.setTextContent(new graphicUtil.Text(textContentOption)); + } + } + + if (el) { + const clipPathOption = elOption.clipPath; + if (clipPathOption) { + const clipPathType = clipPathOption.type; + let clipPath: graphicUtil.Path; + let isInit = false; + if (isMerge) { + const oldClipPath = el.getClipPath(); + isInit = !oldClipPath + || inner(oldClipPath).type !== clipPathType; + clipPath = isInit ? newEl(clipPathType) as graphicUtil.Path : oldClipPath; + } + else if (isReplace) { + isInit = true; + clipPath = newEl(clipPathType) as graphicUtil.Path; + } + + el.setClipPath(clipPath); + + applyUpdateTransition( + clipPath, + clipPathOption, + graphicModel, + { isInit} + ); + applyKeyframeAnimation( + clipPath, + clipPathOption.keyframeAnimation, + graphicModel + ); + } + + const elInner = inner(el); + + el.setTextConfig(textConfig); + + elInner.option = elOption; + setEventData(el, graphicModel, elOption); + + graphicUtil.setTooltipConfig({ + el: el, + componentModel: graphicModel, + itemName: el.name, + itemTooltipOption: elOption.tooltip + }); + + applyKeyframeAnimation(el, elOption.keyframeAnimation, graphicModel); + } + }); + } + + /** + * Locate graphic elements. + */ + private _relocate(graphicModel: GraphicComponentModel, api: ExtensionAPI): void { + const elOptions = graphicModel.option.elements; + const rootGroup = this.group; + const elMap = this._elMap; + const apiWidth = api.getWidth(); + const apiHeight = api.getHeight(); + + const xy = ['x', 'y'] as const; + + // Top-down to calculate percentage width/height of group + for (let i = 0; i < elOptions.length; i++) { + const elOption = elOptions[i]; + const id = modelUtil.convertOptionIdName(elOption.id, null); + const el = id != null ? elMap.get(id) : null; + + if (!el || !el.isGroup) { + continue; + } + const parentEl = el.parent; + const isParentRoot = parentEl === rootGroup; + // Like 'position:absolut' in css, default 0. + const elInner = inner(el); + const parentElInner = inner(parentEl); + elInner.width = parsePercent( + (elInner.option as GraphicComponentGroupOption).width, + isParentRoot ? apiWidth : parentElInner.width + ) || 0; + elInner.height = parsePercent( + (elInner.option as GraphicComponentGroupOption).height, + isParentRoot ? apiHeight : parentElInner.height + ) || 0; + } + + // Bottom-up tranvese all elements (consider ec resize) to locate elements. + for (let i = elOptions.length - 1; i >= 0; i--) { + const elOption = elOptions[i]; + const id = modelUtil.convertOptionIdName(elOption.id, null); + const el = id != null ? elMap.get(id) : null; + + if (!el) { + continue; + } + + const parentEl = el.parent; + const parentElInner = inner(parentEl); + const containerInfo = parentEl === rootGroup + ? { + width: apiWidth, + height: apiHeight + } + : { + width: parentElInner.width, + height: parentElInner.height + }; + + // PENDING + // Currently, when `bounding: 'all'`, the union bounding rect of the group + // does not include the rect of [0, 0, group.width, group.height], which + // is probably weird for users. Should we make a break change for it? + const layoutPos = {} as Record<'x' | 'y', number>; + const layouted = layoutUtil.positionElement( + el, elOption, containerInfo, null, + { hv: elOption.hv, boundingMode: elOption.bounding }, + layoutPos + ); + + if (!inner(el).isNew && layouted) { + const transition = elOption.transition; + const animatePos = {} as Record<'x' | 'y', number>; + for (let k = 0; k < xy.length; k++) { + const key = xy[k]; + const val = layoutPos[key]; + if (transition && (isTransitionAll(transition) || zrUtil.indexOf(transition, key) >= 0)) { + animatePos[key] = val; + } + else { + el[key] = val; + } + } + updateProps(el, animatePos, graphicModel, 0); + } + else { + el.attr(layoutPos); + } + } + } + + /** + * Clear all elements. + */ + private _clear(): void { + const elMap = this._elMap; + elMap.each((el) => { + removeEl(el, inner(el).option, elMap, this._lastGraphicModel); + }); + this._elMap = zrUtil.createHashMap(); + } + + dispose(): void { + this._clear(); + } +} + +function newEl(graphicType: string) { + if (__DEV__) { + zrUtil.assert(graphicType, 'graphic type MUST be set'); + } + + const Clz = ( + zrUtil.hasOwn(nonShapeGraphicElements, graphicType) + // Those graphic elements are not shapes. They should not be + // overwritten by users, so do them first. + ? nonShapeGraphicElements[graphicType as NonShapeGraphicElementType] + : graphicUtil.getShapeClass(graphicType) + ) as { new(opt: GraphicComponentElementOption): Element; }; + + if (__DEV__) { + zrUtil.assert(Clz, `graphic type ${graphicType} can not be found`); + } + + const el = new Clz({}); + inner(el).type = graphicType; + return el; +} +function createEl( + id: string, + targetElParent: graphicUtil.Group, + graphicType: string, + elMap: ElementMap +): Element { + + const el = newEl(graphicType); + + targetElParent.add(el); + elMap.set(id, el); + inner(el).id = id; + inner(el).isNew = true; + + return el; +} +function removeEl( + elExisting: Element, + elOption: GraphicComponentElementOption, + elMap: ElementMap, + graphicModel: GraphicComponentModel +): void { + const existElParent = elExisting && elExisting.parent; + if (existElParent) { + elExisting.type === 'group' && elExisting.traverse(function (el) { + removeEl(el, elOption, elMap, graphicModel); + }); + applyLeaveTransition(elExisting, elOption, graphicModel); + elMap.removeKey(inner(elExisting).id); + } +} + +function updateCommonAttrs( + el: Element, + elOption: GraphicComponentElementOption, + defaultZ: number, + defaultZlevel: number +) { + if (!el.isGroup) { + const elDisplayable = el as Displayable; + // We should not support configure z and zlevel in the element level. + // But seems we didn't limit it previously. So here still use it to avoid breaking. + elDisplayable.z = zrUtil.retrieve2((elOption as any).z, defaultZ || 0); + elDisplayable.zlevel = zrUtil.retrieve2((elOption as any).zlevel, defaultZlevel || 0); + // z2 must not be null/undefined, otherwise sort error may occur. + const optZ2 = (elOption as GraphicComponentDisplayableOption).z2; + optZ2 != null && (elDisplayable.z2 = optZ2 || 0); + } + + zrUtil.each(zrUtil.keys(elOption), key => { + const val = (elOption as any)[key]; + // Assign event handlers. + // PENDING: should enumerate all event names or use pattern matching? + if (key.indexOf('on') === 0 && zrUtil.isFunction(val)) { + (el as any)[key] = val; + } + }); + el.draggable = elOption.draggable; + + // Other attributes + elOption.name != null && (el.name = elOption.name); + elOption.id != null && ((el as any).id = elOption.id); + +} +// Remove unnecessary props to avoid potential problems. +function getCleanedElOption( + elOption: GraphicComponentElementOption +): Omit { + elOption = zrUtil.extend({}, elOption); + zrUtil.each( + ['id', 'parentId', '$action', 'hv', 'bounding', 'textContent', 'clipPath'].concat(layoutUtil.LOCATION_PARAMS), + function (name) { + delete (elOption as any)[name]; + } + ); + return elOption; +} + +function setEventData( + el: Element, + graphicModel: GraphicComponentModel, + elOption: GraphicComponentElementOption +): void { + let eventData = getECData(el).eventData; + // Simple optimize for large amount of elements that no need event. + if (!el.silent && !el.ignore && !eventData) { + eventData = getECData(el).eventData = { + componentType: 'graphic', + componentIndex: graphicModel.componentIndex, + name: el.name + }; + } + + // `elOption.info` enables user to mount some info on + // elements and use them in event handlers. + if (eventData) { + eventData.info = elOption.info; + } +} diff --git a/src/component/graphic/install.ts b/src/component/graphic/install.ts index bfa2a82cc6..4251932ecc 100644 --- a/src/component/graphic/install.ts +++ b/src/component/graphic/install.ts @@ -18,769 +18,38 @@ */ -import * as zrUtil from 'zrender/src/core/util'; -import * as modelUtil from '../../util/model'; -import * as graphicUtil from '../../util/graphic'; -import * as layoutUtil from '../../util/layout'; -import {parsePercent} from '../../util/number'; -import { - ComponentOption, - BoxLayoutOptionMixin, - Dictionary, - ZRStyleProps, - OptionId, - OptionPreprocessor, - CommonTooltipOption -} from '../../util/types'; -import ComponentModel from '../../model/Component'; -import Element, { ElementTextConfig } from 'zrender/src/Element'; -import Displayable from 'zrender/src/graphic/Displayable'; -import { PathProps } from 'zrender/src/graphic/Path'; -import { ImageStyleProps } from 'zrender/src/graphic/Image'; -import GlobalModel from '../../model/Global'; -import ComponentView from '../../view/Component'; -import ExtensionAPI from '../../core/ExtensionAPI'; -import { getECData } from '../../util/innerStore'; -import { TextStyleProps } from 'zrender/src/graphic/Text'; -import { isEC4CompatibleStyle, convertFromEC4CompatibleStyle } from '../../util/styleCompat'; +import { isArray } from 'zrender/src/core/util'; import { EChartsExtensionInstallRegisters } from '../../extension'; +import { GraphicComponentModel, GraphicComponentOption } from './GraphicModel'; +import { GraphicComponentView } from './GraphicView'; -type TransformProp = 'x' | 'y' | 'scaleX' | 'scaleY' | 'originX' | 'originY' | 'skewX' | 'skewY' | 'rotation'; - -interface GraphicComponentBaseElementOption extends - Partial>, - /** - * left/right/top/bottom: (like 12, '22%', 'center', default undefined) - * If left/rigth is set, shape.x/shape.cx/position will not be used. - * If top/bottom is set, shape.y/shape.cy/position will not be used. - * This mechanism is useful when you want to position a group/element - * against the right side or the center of this container. - */ - Partial> { - - /** - * element type, mandatory. - * Only can be omit if call setOption not at the first time and perform merge. - */ - type?: string; - - id?: OptionId; - name?: string; - - // Only internal usage. Use specified value does NOT make sense. - parentId?: OptionId; - parentOption?: GraphicComponentElementOption; - children?: GraphicComponentElementOption[]; - hv?: [boolean, boolean]; - - /** - * bounding: (enum: 'all' (default) | 'raw') - * Specify how to calculate boundingRect when locating. - * 'all': Get uioned and transformed boundingRect - * from both itself and its descendants. - * This mode simplies confining a group of elements in the bounding - * of their ancester container (e.g., using 'right: 0'). - * 'raw': Only use the boundingRect of itself and before transformed. - * This mode is similar to css behavior, which is useful when you - * want an element to be able to overflow its container. (Consider - * a rotated circle needs to be located in a corner.) - */ - bounding?: 'raw' | 'all'; - - /** - * info: custom info. enables user to mount some info on elements and use them - * in event handlers. Update them only when user specified, otherwise, remain. - */ - info?: GraphicExtraElementInfo; - - textContent?: GraphicComponentTextOption; - textConfig?: ElementTextConfig; - - $action?: 'merge' | 'replace' | 'remove'; - - tooltip?: CommonTooltipOption; -}; - -interface GraphicComponentDisplayableOption extends - GraphicComponentBaseElementOption, - Partial> { - - style?: ZRStyleProps; - - // TODO: states? - // emphasis?: GraphicComponentDisplayableOptionOnState; - // blur?: GraphicComponentDisplayableOptionOnState; - // select?: GraphicComponentDisplayableOptionOnState; -} -// TODO: states? -// interface GraphicComponentDisplayableOptionOnState extends Partial> { -// style?: ZRStyleProps; -// } -interface GraphicComponentGroupOption extends GraphicComponentBaseElementOption { - type?: 'group'; - - /** - * width/height: (can only be pixel value, default 0) - * Only be used to specify contianer(group) size, if needed. And - * can not be percentage value (like '33%'). See the reason in the - * layout algorithm below. - */ - width?: number; - height?: number; - - // TODO: Can only set focus, blur on the root element. - // children: Omit[]; - children: GraphicComponentElementOption[]; -} -export interface GraphicComponentZRPathOption extends GraphicComponentDisplayableOption { - shape?: PathProps['shape']; -} -export interface GraphicComponentImageOption extends GraphicComponentDisplayableOption { - type?: 'image'; - style?: ImageStyleProps; - // TODO: states? - // emphasis?: GraphicComponentImageOptionOnState; - // blur?: GraphicComponentImageOptionOnState; - // select?: GraphicComponentImageOptionOnState; -} -// TODO: states? -// interface GraphicComponentImageOptionOnState extends GraphicComponentDisplayableOptionOnState { -// style?: ImageStyleProps; -// } -interface GraphicComponentTextOption - extends Omit { - type?: 'text'; - style?: TextStyleProps; -} -type GraphicComponentElementOption = - GraphicComponentGroupOption - | GraphicComponentZRPathOption - | GraphicComponentImageOption - | GraphicComponentTextOption; -// type GraphicComponentElementOptionOnState = -// GraphicComponentDisplayableOptionOnState -// | GraphicComponentImageOptionOnState; - -type GraphicExtraElementInfo = Dictionary; - -type ElementMap = zrUtil.HashMap; - -const inner = modelUtil.makeInner<{ - widthOption: number; - heightOption: number; - width: number; - height: number; - id: string; -}, Element>(); - - -const _nonShapeGraphicElements = { - - // Reserved but not supported in graphic component. - path: null as unknown, - compoundPath: null as unknown, - - // Supported in graphic component. - group: graphicUtil.Group, - image: graphicUtil.Image, - text: graphicUtil.Text -} as const; -type NonShapeGraphicElementType = keyof typeof _nonShapeGraphicElements; - -// ------------------------ -// Preprocessor -// ------------------------ - -const preprocessor: OptionPreprocessor = function (option) { - const graphicOption = option.graphic as GraphicComponentOption | GraphicComponentOption[]; - - // Convert - // {graphic: [{left: 10, type: 'circle'}, ...]} - // or - // {graphic: {left: 10, type: 'circle'}} - // to - // {graphic: [{elements: [{left: 10, type: 'circle'}, ...]}]} - if (zrUtil.isArray(graphicOption)) { - if (!graphicOption[0] || !graphicOption[0].elements) { - option.graphic = [{elements: graphicOption}]; - } - else { - // Only one graphic instance can be instantiated. (We dont - // want that too many views are created in echarts._viewMap) - option.graphic = [(option.graphic as any)[0]]; - } - } - else if (graphicOption && !graphicOption.elements) { - option.graphic = [{elements: [graphicOption]}]; - } -}; - -// ------------------------ -// Model -// ------------------------ - -export type GraphicComponentLooseOption = (GraphicComponentOption | GraphicComponentElementOption) & { - mainType?: 'graphic'; -}; - -export interface GraphicComponentOption extends ComponentOption { - // Note: elements is always behind its ancestors in this elements array. - elements?: GraphicComponentElementOption[]; - // parentId: string; -}; - - -class GraphicComponentModel extends ComponentModel { - - static type = 'graphic'; - type = GraphicComponentModel.type; - - preventAutoZ = true; - - static defaultOption: GraphicComponentOption = { - elements: [] - // parentId: null - }; - - /** - * Save el options for the sake of the performance (only update modified graphics). - * The order is the same as those in option. (ancesters -> descendants) - */ - private _elOptionsToUpdate: GraphicComponentElementOption[]; - - mergeOption(option: GraphicComponentOption, ecModel: GlobalModel): void { - // Prevent default merge to elements - const elements = this.option.elements; - this.option.elements = null; - - super.mergeOption(option, ecModel); - - this.option.elements = elements; - } - - optionUpdated(newOption: GraphicComponentOption, isInit: boolean): void { - const thisOption = this.option; - const newList = (isInit ? thisOption : newOption).elements; - const existList = thisOption.elements = isInit ? [] : thisOption.elements; - - const flattenedList = [] as GraphicComponentElementOption[]; - this._flatten(newList, flattenedList, null); - - const mappingResult = modelUtil.mappingToExists(existList, flattenedList, 'normalMerge'); - - // Clear elOptionsToUpdate - const elOptionsToUpdate = this._elOptionsToUpdate = [] as GraphicComponentElementOption[]; - - zrUtil.each(mappingResult, function (resultItem, index) { - const newElOption = resultItem.newOption as GraphicComponentElementOption; - - if (__DEV__) { - zrUtil.assert( - zrUtil.isObject(newElOption) || resultItem.existing, - 'Empty graphic option definition' - ); - } - - if (!newElOption) { - return; - } - - elOptionsToUpdate.push(newElOption); - - setKeyInfoToNewElOption(resultItem, newElOption); - - mergeNewElOptionToExist(existList, index, newElOption); - - setLayoutInfoToExist(existList[index], newElOption); - - }, this); - - // Clean - thisOption.elements = zrUtil.filter(existList, (item) => { - // $action should be volatile, otherwise option gotten from - // `getOption` will contain unexpected $action. - item && delete item.$action; - return item != null; - }); - } - - /** - * Convert - * [{ - * type: 'group', - * id: 'xx', - * children: [{type: 'circle'}, {type: 'polygon'}] - * }] - * to - * [ - * {type: 'group', id: 'xx'}, - * {type: 'circle', parentId: 'xx'}, - * {type: 'polygon', parentId: 'xx'} - * ] - */ - private _flatten( - optionList: GraphicComponentElementOption[], - result: GraphicComponentElementOption[], - parentOption: GraphicComponentElementOption - ): void { - zrUtil.each(optionList, function (option) { - if (!option) { - return; - } - - if (parentOption) { - option.parentOption = parentOption; - } - - result.push(option); - - const children = option.children; - if (option.type === 'group' && children) { - this._flatten(children, result, option); - } - // Deleting for JSON output, and for not affecting group creation. - delete option.children; - }, this); - } - - // FIXME - // Pass to view using payload? setOption has a payload? - useElOptionsToUpdate(): GraphicComponentElementOption[] { - const els = this._elOptionsToUpdate; - // Clear to avoid render duplicately when zooming. - this._elOptionsToUpdate = null; - return els; - } -} - -// ------------------------ -// View -// ------------------------ - -class GraphicComponentView extends ComponentView { - - static type = 'graphic'; - type = GraphicComponentView.type; - - private _elMap: ElementMap; - private _lastGraphicModel: GraphicComponentModel; - - init() { - this._elMap = zrUtil.createHashMap(); - } - - render(graphicModel: GraphicComponentModel, ecModel: GlobalModel, api: ExtensionAPI): void { - - // Having leveraged between use cases and algorithm complexity, a very - // simple layout mechanism is used: - // The size(width/height) can be determined by itself or its parent (not - // implemented yet), but can not by its children. (Top-down travel) - // The location(x/y) can be determined by the bounding rect of itself - // (can including its descendants or not) and the size of its parent. - // (Bottom-up travel) - - // When `chart.clear()` or `chart.setOption({...}, true)` with the same id, - // view will be reused. - if (graphicModel !== this._lastGraphicModel) { - this._clear(); - } - this._lastGraphicModel = graphicModel; - - this._updateElements(graphicModel); - this._relocate(graphicModel, api); - } - - /** - * Update graphic elements. - */ - private _updateElements(graphicModel: GraphicComponentModel): void { - const elOptionsToUpdate = graphicModel.useElOptionsToUpdate(); - - if (!elOptionsToUpdate) { - return; - } - - const elMap = this._elMap; - const rootGroup = this.group; - - // Top-down tranverse to assign graphic settings to each elements. - zrUtil.each(elOptionsToUpdate, function (elOption) { - const id = modelUtil.convertOptionIdName(elOption.id, null); - const elExisting = id != null ? elMap.get(id) : null; - const parentId = modelUtil.convertOptionIdName(elOption.parentId, null); - const targetElParent = (parentId != null ? elMap.get(parentId) : rootGroup) as graphicUtil.Group; - - const elType = elOption.type; - const elOptionStyle = (elOption as GraphicComponentDisplayableOption).style; - if (elType === 'text' && elOptionStyle) { - // In top/bottom mode, textVerticalAlign should not be used, which cause - // inaccurately locating. - if (elOption.hv && elOption.hv[1]) { - (elOptionStyle as any).textVerticalAlign = - (elOptionStyle as any).textBaseline = - (elOptionStyle as TextStyleProps).verticalAlign = - (elOptionStyle as TextStyleProps).align = null; - } - } - - let textContentOption = (elOption as GraphicComponentZRPathOption).textContent; - let textConfig = (elOption as GraphicComponentZRPathOption).textConfig; - if (elOptionStyle - && isEC4CompatibleStyle(elOptionStyle, elType, !!textConfig, !!textContentOption) - ) { - const convertResult = - convertFromEC4CompatibleStyle(elOptionStyle, elType, true) as GraphicComponentZRPathOption; - if (!textConfig && convertResult.textConfig) { - textConfig = (elOption as GraphicComponentZRPathOption).textConfig = convertResult.textConfig; - } - if (!textContentOption && convertResult.textContent) { - textContentOption = convertResult.textContent; - } - } - - // Remove unnecessary props to avoid potential problems. - const elOptionCleaned = getCleanedElOption(elOption); - - // For simple, do not support parent change, otherwise reorder is needed. - if (__DEV__) { - elExisting && zrUtil.assert( - targetElParent === elExisting.parent, - 'Changing parent is not supported.' - ); - } - - const $action = elOption.$action || 'merge'; - if ($action === 'merge') { - elExisting - ? elExisting.attr(elOptionCleaned) - : createEl(id, targetElParent, elOptionCleaned, elMap); - } - else if ($action === 'replace') { - removeEl(elExisting, elMap); - createEl(id, targetElParent, elOptionCleaned, elMap); - } - else if ($action === 'remove') { - removeEl(elExisting, elMap); - } - - const el = elMap.get(id); +export function install(registers: EChartsExtensionInstallRegisters) { - if (el && textContentOption) { - if ($action === 'merge') { - const textContentExisting = el.getTextContent(); - textContentExisting - ? textContentExisting.attr(textContentOption) - : el.setTextContent(new graphicUtil.Text(textContentOption)); - } - else if ($action === 'replace') { - el.setTextContent(new graphicUtil.Text(textContentOption)); - } - } + registers.registerComponentModel(GraphicComponentModel); + registers.registerComponentView(GraphicComponentView); - if (el) { - const elInner = inner(el); - elInner.widthOption = (elOption as GraphicComponentGroupOption).width; - elInner.heightOption = (elOption as GraphicComponentGroupOption).height; - setEventData(el, graphicModel, elOption); + registers.registerPreprocessor(function (option) { + const graphicOption = option.graphic as GraphicComponentOption | GraphicComponentOption[]; - graphicUtil.setTooltipConfig({ - el: el, - componentModel: graphicModel, - itemName: el.name, - itemTooltipOption: elOption.tooltip - }); + // Convert + // {graphic: [{left: 10, type: 'circle'}, ...]} + // or + // {graphic: {left: 10, type: 'circle'}} + // to + // {graphic: [{elements: [{left: 10, type: 'circle'}, ...]}]} + if (isArray(graphicOption)) { + if (!graphicOption[0] || !graphicOption[0].elements) { + option.graphic = [{ elements: graphicOption }]; } - }); - } - - /** - * Locate graphic elements. - */ - private _relocate(graphicModel: GraphicComponentModel, api: ExtensionAPI): void { - const elOptions = graphicModel.option.elements; - const rootGroup = this.group; - const elMap = this._elMap; - const apiWidth = api.getWidth(); - const apiHeight = api.getHeight(); - - // Top-down to calculate percentage width/height of group - for (let i = 0; i < elOptions.length; i++) { - const elOption = elOptions[i]; - const id = modelUtil.convertOptionIdName(elOption.id, null); - const el = id != null ? elMap.get(id) : null; - - if (!el || !el.isGroup) { - continue; + else { + // Only one graphic instance can be instantiated. (We dont + // want that too many views are created in echarts._viewMap) + option.graphic = [(option.graphic as any)[0]]; } - const parentEl = el.parent; - const isParentRoot = parentEl === rootGroup; - // Like 'position:absolut' in css, default 0. - const elInner = inner(el); - const parentElInner = inner(parentEl); - elInner.width = parsePercent( - elInner.widthOption, - isParentRoot ? apiWidth : parentElInner.width - ) || 0; - elInner.height = parsePercent( - elInner.heightOption, - isParentRoot ? apiHeight : parentElInner.height - ) || 0; } - - // Bottom-up tranvese all elements (consider ec resize) to locate elements. - for (let i = elOptions.length - 1; i >= 0; i--) { - const elOption = elOptions[i]; - const id = modelUtil.convertOptionIdName(elOption.id, null); - const el = id != null ? elMap.get(id) : null; - - if (!el) { - continue; - } - - const parentEl = el.parent; - const parentElInner = inner(parentEl); - const containerInfo = parentEl === rootGroup - ? { - width: apiWidth, - height: apiHeight - } - : { - width: parentElInner.width, - height: parentElInner.height - }; - - // PENDING - // Currently, when `bounding: 'all'`, the union bounding rect of the group - // does not include the rect of [0, 0, group.width, group.height], which - // is probably weird for users. Should we make a break change for it? - layoutUtil.positionElement( - el, elOption, containerInfo, null, - {hv: elOption.hv, boundingMode: elOption.bounding} - ); - } - } - - /** - * Clear all elements. - */ - private _clear(): void { - const elMap = this._elMap; - elMap.each(function (el) { - removeEl(el, elMap); - }); - this._elMap = zrUtil.createHashMap(); - } - - dispose(): void { - this._clear(); - } -} - -function createEl( - id: string, - targetElParent: graphicUtil.Group, - elOption: GraphicComponentElementOption, - elMap: ElementMap -): void { - const graphicType = elOption.type; - - if (__DEV__) { - zrUtil.assert(graphicType, 'graphic type MUST be set'); - } - - const Clz = ( - zrUtil.hasOwn(_nonShapeGraphicElements, graphicType) - // Those graphic elements are not shapes. They should not be - // overwritten by users, so do them first. - ? _nonShapeGraphicElements[graphicType as NonShapeGraphicElementType] - : graphicUtil.getShapeClass(graphicType) - ) as { new(opt: GraphicComponentElementOption): Element }; - - if (__DEV__) { - zrUtil.assert(Clz, 'graphic type can not be found'); - } - - const el = new Clz(elOption); - targetElParent.add(el); - elMap.set(id, el); - inner(el).id = id; -} - -function removeEl(elExisting: Element, elMap: ElementMap): void { - const existElParent = elExisting && elExisting.parent; - if (existElParent) { - elExisting.type === 'group' && elExisting.traverse(function (el) { - removeEl(el, elMap); - }); - elMap.removeKey(inner(elExisting).id); - existElParent.remove(elExisting); - } -} - -// Remove unnecessary props to avoid potential problems. -function getCleanedElOption( - elOption: GraphicComponentElementOption -): Omit { - elOption = zrUtil.extend({}, elOption); - zrUtil.each( - ['id', 'parentId', '$action', 'hv', 'bounding', 'textContent'].concat(layoutUtil.LOCATION_PARAMS), - function (name) { - delete (elOption as any)[name]; + else if (graphicOption && !graphicOption.elements) { + option.graphic = [{ elements: [graphicOption] }]; } - ); - return elOption; -} - -function isSetLoc( - obj: GraphicComponentElementOption, - props: ('left' | 'right' | 'top' | 'bottom')[] -): boolean { - let isSet; - zrUtil.each(props, function (prop) { - obj[prop] != null && obj[prop] !== 'auto' && (isSet = true); }); - return isSet; -} - -function setKeyInfoToNewElOption( - resultItem: ReturnType[number], - newElOption: GraphicComponentElementOption -): void { - const existElOption = resultItem.existing as GraphicComponentElementOption; - - // Set id and type after id assigned. - newElOption.id = resultItem.keyInfo.id; - !newElOption.type && existElOption && (newElOption.type = existElOption.type); - - // Set parent id if not specified - if (newElOption.parentId == null) { - const newElParentOption = newElOption.parentOption; - if (newElParentOption) { - newElOption.parentId = newElParentOption.id; - } - else if (existElOption) { - newElOption.parentId = existElOption.parentId; - } - } - - // Clear - newElOption.parentOption = null; -} - -function mergeNewElOptionToExist( - existList: GraphicComponentElementOption[], - index: number, - newElOption: GraphicComponentElementOption -): void { - // Update existing options, for `getOption` feature. - const newElOptCopy = zrUtil.extend({}, newElOption); - const existElOption = existList[index]; - - const $action = newElOption.$action || 'merge'; - if ($action === 'merge') { - if (existElOption) { - - if (__DEV__) { - const newType = newElOption.type; - zrUtil.assert( - !newType || existElOption.type === newType, - 'Please set $action: "replace" to change `type`' - ); - } - - // We can ensure that newElOptCopy and existElOption are not - // the same object, so `merge` will not change newElOptCopy. - zrUtil.merge(existElOption, newElOptCopy, true); - // Rigid body, use ignoreSize. - layoutUtil.mergeLayoutParam(existElOption, newElOptCopy, {ignoreSize: true}); - // Will be used in render. - layoutUtil.copyLayoutParams(newElOption, existElOption); - } - else { - existList[index] = newElOptCopy; - } - } - else if ($action === 'replace') { - existList[index] = newElOptCopy; - } - else if ($action === 'remove') { - // null will be cleaned later. - existElOption && (existList[index] = null); - } -} - -function setLayoutInfoToExist( - existItem: GraphicComponentElementOption, - newElOption: GraphicComponentElementOption -) { - if (!existItem) { - return; - } - existItem.hv = newElOption.hv = [ - // Rigid body, dont care `width`. - isSetLoc(newElOption, ['left', 'right']), - // Rigid body, dont care `height`. - isSetLoc(newElOption, ['top', 'bottom']) - ]; - // Give default group size. Otherwise layout error may occur. - if (existItem.type === 'group') { - const existingGroupOpt = existItem as GraphicComponentGroupOption; - const newGroupOpt = newElOption as GraphicComponentGroupOption; - existingGroupOpt.width == null && (existingGroupOpt.width = newGroupOpt.width = 0); - existingGroupOpt.height == null && (existingGroupOpt.height = newGroupOpt.height = 0); - } -} - -function setEventData( - el: Element, - graphicModel: GraphicComponentModel, - elOption: GraphicComponentElementOption -): void { - let eventData = getECData(el).eventData; - // Simple optimize for large amount of elements that no need event. - if (!el.silent && !el.ignore && !eventData) { - eventData = getECData(el).eventData = { - componentType: 'graphic', - componentIndex: graphicModel.componentIndex, - name: el.name - }; - } - - // `elOption.info` enables user to mount some info on - // elements and use them in event handlers. - if (eventData) { - eventData.info = elOption.info; - } -} - -export function install(registers: EChartsExtensionInstallRegisters) { - registers.registerComponentModel(GraphicComponentModel); - registers.registerComponentView(GraphicComponentView); - registers.registerPreprocessor(preprocessor); } \ No newline at end of file diff --git a/src/component/helper/MapDraw.ts b/src/component/helper/MapDraw.ts index 56bf2c54bc..f9a97f85a4 100644 --- a/src/component/helper/MapDraw.ts +++ b/src/component/helper/MapDraw.ts @@ -540,7 +540,10 @@ class MapDraw { api.dispatchAction(zrUtil.extend(makeActionBase(), { dx: e.dx, - dy: e.dy + dy: e.dy, + animation: { + duration: 0 + } })); }, this); @@ -552,7 +555,10 @@ class MapDraw { api.dispatchAction(zrUtil.extend(makeActionBase(), { zoom: e.scale, originX: e.originX, - originY: e.originY + originY: e.originY, + animation: { + duration: 0 + } })); }, this); diff --git a/src/data/helper/transform.ts b/src/data/helper/transform.ts index 6e5181f140..9abce7e671 100644 --- a/src/data/helper/transform.ts +++ b/src/data/helper/transform.ts @@ -32,7 +32,7 @@ import { getRawSourceItemGetter, getRawSourceDataCounter, getRawSourceValueGetter } from './dataProvider'; import { parseDataValue } from './dataValueHelper'; -import { consoleLog, makePrintable, throwError } from '../../util/log'; +import { log, makePrintable, throwError } from '../../util/log'; import { createSource, Source, SourceMetaRawOption, detectSourceFormat } from '../Source'; @@ -443,7 +443,7 @@ function applySingleDataTransform( makePrintable(extSource.dimensions) ].join('\n'); }).join('\n'); - consoleLog(printStrArr); + log(printStrArr); } } diff --git a/src/export/option.ts b/src/export/option.ts index dff46af9d9..d25e8197a3 100644 --- a/src/export/option.ts +++ b/src/export/option.ts @@ -94,7 +94,7 @@ import type { CustomSeriesRenderItem } from '../chart/custom/CustomSeries'; -import type { GraphicComponentLooseOption as GraphicComponentOption } from '../component/graphic/install'; +import { GraphicComponentLooseOption as GraphicComponentOption } from '../component/graphic/GraphicModel'; import type { DatasetOption as DatasetComponentOption } from '../component/dataset/install'; import type {ToolboxBrushFeatureOption} from '../component/toolbox/feature/Brush'; diff --git a/src/util/layout.ts b/src/util/layout.ts index 7e4cddcd09..1c30e72948 100644 --- a/src/util/layout.ts +++ b/src/util/layout.ts @@ -308,6 +308,8 @@ export function getLayoutRect( * * If be called repeatly with the same input el, the same result will be gotten. * + * Return true if the layout happend. + * * @param el Should have `getBoundingRect` method. * @param positionInfo * @param positionInfo.left @@ -339,14 +341,19 @@ export function positionElement( opt?: { hv: [1 | 0 | boolean, 1 | 0 | boolean], boundingMode: 'all' | 'raw' - } -) { + }, + out?: { x?: number, y?: number } +): boolean { const h = !opt || !opt.hv || opt.hv[0]; const v = !opt || !opt.hv || opt.hv[1]; const boundingMode = opt && opt.boundingMode || 'all'; + out = out || el; + + out.x = el.x; + out.y = el.y; if (!h && !v) { - return; + return false; } let rect; @@ -383,14 +390,17 @@ export function positionElement( const dy = v ? layoutRect.y - rect.y : 0; if (boundingMode === 'raw') { - el.x = dx; - el.y = dy; + out.x = dx; + out.y = dy; } else { - el.x += dx; - el.y += dy; + out.x += dx; + out.y += dy; + } + if (out === el) { + el.markRedraw(); } - el.markRedraw(); + return true; } /** diff --git a/src/util/log.ts b/src/util/log.ts index 527051c247..7d8ca56285 100644 --- a/src/util/log.ts +++ b/src/util/log.ts @@ -27,34 +27,35 @@ const hasConsole = typeof console !== 'undefined' // eslint-disable-next-line && console.warn && console.log; -export function log(str: string) { +function outputLog(type: 'log' | 'warn' | 'error', str: string, onlyOnce?: boolean) { if (hasConsole) { + if (onlyOnce) { + if (storedLogs[str]) { + return; + } + storedLogs[str] = true; + } // eslint-disable-next-line - console.log(ECHARTS_PREFIX + str); + console[type](ECHARTS_PREFIX + str); } } -export function warn(str: string) { - if (hasConsole) { - console.warn(ECHARTS_PREFIX + str); - } +export function log(str: string, onlyOnce?: boolean) { + outputLog('log', str, onlyOnce); } -export function error(str: string) { - if (hasConsole) { - console.error(ECHARTS_PREFIX + str); - } +export function warn(str: string, onlyOnce?: boolean) { + outputLog('warn', str, onlyOnce); +} + +export function error(str: string, onlyOnce?: boolean) { + outputLog('error', str, onlyOnce); } export function deprecateLog(str: string) { if (__DEV__) { - if (storedLogs[str]) { // Not display duplicate message. - return; - } - if (hasConsole) { - storedLogs[str] = true; - console.warn(ECHARTS_PREFIX + 'DEPRECATED: ' + str); - } + // Not display duplicate message. + outputLog('warn', 'DEPRECATED: ' + str, true); } } @@ -64,16 +65,6 @@ export function deprecateReplaceLog(oldOpt: string, newOpt: string, scope?: stri } } -export function consoleLog(...args: unknown[]) { - if (__DEV__) { - /* eslint-disable no-console */ - if (typeof console !== 'undefined' && console.log) { - console.log.apply(console, args); - } - /* eslint-enable no-console */ - } -} - /** * If in __DEV__ environment, get console printable message for users hint. * Parameters are separated by ' '. diff --git a/src/util/model.ts b/src/util/model.ts index 9f78c1583e..2495723049 100644 --- a/src/util/model.ts +++ b/src/util/model.ts @@ -51,9 +51,12 @@ import SeriesModel from '../model/Series'; import CartesianAxisModel from '../coord/cartesian/AxisModel'; import GridModel from '../coord/cartesian/GridModel'; import { isNumeric, getRandomIdBase, getPrecision, round } from './number'; -import { interpolateNumber } from 'zrender/src/animation/Animator'; import { warn } from './log'; +function interpolateNumber(p0: number, p1: number, percent: number): number { + return (p1 - p0) * percent + p0; +} + /** * Make the name displayable. But we should * make sure it is not duplicated with user diff --git a/src/util/states.ts b/src/util/states.ts index 3cff89901f..29179f4b2d 100644 --- a/src/util/states.ts +++ b/src/util/states.ts @@ -38,7 +38,18 @@ import { DownplayPayload, ComponentMainType } from './types'; -import { extend, indexOf, isArrayLike, isObject, keys, isArray, each } from 'zrender/src/core/util'; +import { + extend, + indexOf, + isArrayLike, + isObject, + keys, + isArray, + each, + isString, + isGradientObject, + map +} from 'zrender/src/core/util'; import { getECData } from './innerStore'; import * as colorTool from 'zrender/src/tool/color'; import SeriesData from '../data/SeriesData'; @@ -96,16 +107,25 @@ function hasFillOrStroke(fillOrStroke: string | PatternObject | GradientObject) } // Most lifted color are duplicated. const liftedColorCache = new LRU(100); -function liftColor(color: string): string { - if (typeof color !== 'string') { - return color; - } - let liftedColor = liftedColorCache.get(color); - if (!liftedColor) { - liftedColor = colorTool.lift(color, -0.1); - liftedColorCache.put(color, liftedColor); +function liftColor(color: GradientObject): GradientObject; +function liftColor(color: string): string; +function liftColor(color: string | GradientObject): string | GradientObject { + if (isString(color)) { + let liftedColor = liftedColorCache.get(color); + if (!liftedColor) { + liftedColor = colorTool.lift(color, -0.1); + liftedColorCache.put(color, liftedColor); + } + return liftedColor; + } + else if (isGradientObject(color)) { + const ret = extend({}, color) as GradientObject; + ret.colorStops = map(color.colorStops, stop => ({ + offset: stop.offset, + color: colorTool.lift(stop.color, -0.1) + })); + return ret; } - return liftedColor; } function doChangeHoverState(el: ECElement, stateName: DisplayState, hoverStateEnum: 0 | 1 | 2) { diff --git a/src/util/types.ts b/src/util/types.ts index ef9938bbaf..95e5b5027d 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -765,60 +765,6 @@ export interface ColorPaletteOptionMixin { color?: ZRColor | ZRColor[] colorLayer?: ZRColor[][] } - -export interface AriaLabelOption { - enabled?: boolean; - description?: string; - general?: { - withTitle?: string; - withoutTitle?: string; - }; - series?: { - maxCount?: number; - single?: { - prefix?: string; - withName?: string; - withoutName?: string; - }; - multiple?: { - prefix?: string; - withName?: string; - withoutName?: string; - separator?: { - middle?: string; - end?: string; - } - } - }; - data?: { - maxCount?: number; - allData?: string; - partialData?: string; - withName?: string; - withoutName?: string; - separator?: { - middle?: string; - end?: string; - } - } -} - -// Extending is for compating ECharts 4 -export interface AriaOption extends AriaLabelOption { - mainType?: 'aria'; - - enabled?: boolean; - label?: AriaLabelOption; - decal?: { - show?: boolean; - decals?: DecalObject | DecalObject[]; - }; -} - -export interface AriaOptionMixin { - aria?: AriaOption -} - /** * Mixin of option set to control the box layout of each component. */ @@ -874,6 +820,7 @@ export interface AnimationOption { delay?: number // additive?: boolean } + /** * Mixin of option set to control the animation of series. */ @@ -1718,3 +1665,58 @@ export interface SeriesEncodeOptionMixin { } export type SeriesEncodableModel = SeriesModel; + + +// TODO Move to aria component +export interface AriaLabelOption { + enabled?: boolean; + description?: string; + general?: { + withTitle?: string; + withoutTitle?: string; + }; + series?: { + maxCount?: number; + single?: { + prefix?: string; + withName?: string; + withoutName?: string; + }; + multiple?: { + prefix?: string; + withName?: string; + withoutName?: string; + separator?: { + middle?: string; + end?: string; + } + } + }; + data?: { + maxCount?: number; + allData?: string; + partialData?: string; + withName?: string; + withoutName?: string; + separator?: { + middle?: string; + end?: string; + } + } +} + +// Extending is for compating ECharts 4 +export interface AriaOption extends AriaLabelOption { + mainType?: 'aria'; + + enabled?: boolean; + label?: AriaLabelOption; + decal?: { + show?: boolean; + decals?: DecalObject | DecalObject[]; + }; +} + +export interface AriaOptionMixin { + aria?: AriaOption +} diff --git a/test/custom-animation.html b/test/custom-animation.html new file mode 100644 index 0000000000..29131bdd93 --- /dev/null +++ b/test/custom-animation.html @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + diff --git a/test/custom-transition2.html b/test/custom-transition2.html index 7c03dad566..e28095d056 100644 --- a/test/custom-transition2.html +++ b/test/custom-transition2.html @@ -214,7 +214,7 @@ var textOpt = { type: 'text', extra: { }, - transition: [], // disable the default transition of x y. + // transition: [], // disable the default transition of x y. style: { x: 20, y: 20, fontSize: 20, stroke: 'green' }, during: function (apiDuring) { var x = apiDuring.getExtra('x'); @@ -227,7 +227,7 @@ shape: { cx: 0, cy: 0, r: 10 }, extra: { }, style: { fill: 'red' }, - transition: [], // disable the default transition of x y. + // transition: [], // disable the default transition of x y. during: function (apiDuring) { var x = apiDuring.getExtra('x'); var y = apiDuring.getExtra('y'); diff --git a/test/graphic-animation-wave.html b/test/graphic-animation-wave.html new file mode 100644 index 0000000000..44a48648d8 --- /dev/null +++ b/test/graphic-animation-wave.html @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + diff --git a/test/graphic-animation.html b/test/graphic-animation.html new file mode 100644 index 0000000000..d112dd8e9e --- /dev/null +++ b/test/graphic-animation.html @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + diff --git a/test/graphic-cases.html b/test/graphic-cases.html index 625a1c1b68..8b913ed9ba 100644 --- a/test/graphic-cases.html +++ b/test/graphic-cases.html @@ -38,17 +38,18 @@
-
+
+
+ diff --git a/test/graphic-transition.html b/test/graphic-transition.html new file mode 100644 index 0000000000..4f057d39ab --- /dev/null +++ b/test/graphic-transition.html @@ -0,0 +1,390 @@ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/lib/testHelper.js b/test/lib/testHelper.js index 7d4908475c..7d17bf34b2 100644 --- a/test/lib/testHelper.js +++ b/test/lib/testHelper.js @@ -115,9 +115,7 @@ + ''; } - if (opt.option) { - chart = testHelper.createChart(echarts, chartContainer, opt.option, opt, opt.setOptionOpts); - } + chart = testHelper.createChart(echarts, chartContainer, opt.option, opt, opt.setOptionOpts); var dataTables = opt.dataTables; if (!dataTables && opt.dataTable) { diff --git a/test/runTest/actions/__meta__.json b/test/runTest/actions/__meta__.json index d90d96fcbd..047c3de96c 100644 --- a/test/runTest/actions/__meta__.json +++ b/test/runTest/actions/__meta__.json @@ -96,7 +96,10 @@ "graph": 2, "graph-grid": 1, "graph-simple": 2, + "graphic-animation": 1, + "graphic-animation-wave": 1, "graphic-draggable": 1, + "graphic-transition": 3, "heatmap": 1, "heatmap-map": 1, "homepage3": 1, diff --git a/test/runTest/actions/graphic-animation-wave.json b/test/runTest/actions/graphic-animation-wave.json new file mode 100644 index 0000000000..1078646775 --- /dev/null +++ b/test/runTest/actions/graphic-animation-wave.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousedown","time":329,"x":677,"y":46},{"type":"mouseup","time":469,"x":677,"y":46},{"time":470,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":845,"x":677,"y":47},{"type":"mousemove","time":1046,"x":659,"y":68},{"type":"mousemove","time":1251,"x":650,"y":69},{"type":"mousemove","time":1430,"x":651,"y":69},{"type":"mousedown","time":1545,"x":652,"y":69},{"type":"mouseup","time":1635,"x":652,"y":69},{"time":1636,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1638,"x":652,"y":69},{"type":"mousemove","time":1945,"x":653,"y":70},{"type":"mousemove","time":2146,"x":677,"y":84},{"type":"mousemove","time":2347,"x":692,"y":92},{"type":"mousemove","time":2550,"x":699,"y":92},{"type":"mousemove","time":2796,"x":700,"y":92},{"type":"mousemove","time":2946,"x":699,"y":93},{"type":"mousemove","time":3146,"x":685,"y":97},{"type":"mousemove","time":3351,"x":680,"y":97},{"type":"mousedown","time":3410,"x":680,"y":97},{"type":"mouseup","time":3509,"x":680,"y":98},{"time":3510,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3652,"x":680,"y":98},{"type":"mousemove","time":3779,"x":680,"y":98},{"type":"mousemove","time":3979,"x":658,"y":119},{"type":"mousemove","time":4184,"x":651,"y":124},{"type":"mousemove","time":4396,"x":646,"y":126},{"type":"mousemove","time":4596,"x":645,"y":126},{"type":"mousedown","time":4693,"x":645,"y":126},{"type":"mouseup","time":4846,"x":645,"y":126},{"time":4847,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5780,"x":646,"y":127},{"type":"mousemove","time":5980,"x":662,"y":139},{"type":"mousemove","time":6186,"x":677,"y":147},{"type":"mousemove","time":6396,"x":687,"y":148},{"type":"mousemove","time":6602,"x":691,"y":149},{"type":"mousedown","time":6656,"x":691,"y":149},{"type":"mouseup","time":6736,"x":691,"y":149},{"time":6737,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6813,"x":693,"y":149},{"type":"mousemove","time":7963,"x":692,"y":149},{"type":"mousemove","time":8163,"x":683,"y":152},{"type":"mousemove","time":8368,"x":658,"y":156},{"type":"mousemove","time":8579,"x":646,"y":155},{"type":"mousemove","time":8780,"x":644,"y":155},{"type":"mousedown","time":8790,"x":644,"y":155},{"type":"mouseup","time":8919,"x":644,"y":155},{"time":8920,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9463,"x":644,"y":155},{"type":"mousemove","time":9663,"x":650,"y":155},{"type":"mousemove","time":9868,"x":657,"y":156},{"type":"mousemove","time":9963,"x":657,"y":156},{"type":"mousemove","time":10163,"x":671,"y":160},{"type":"mousemove","time":10363,"x":680,"y":172},{"type":"mousemove","time":10569,"x":681,"y":174},{"type":"mousemove","time":10730,"x":681,"y":175},{"type":"mousemove","time":10930,"x":678,"y":212},{"type":"mousemove","time":11131,"x":671,"y":225},{"type":"mousemove","time":11336,"x":663,"y":249},{"type":"mousemove","time":11554,"x":662,"y":252},{"type":"mousedown","time":11558,"x":662,"y":252},{"type":"mouseup","time":11647,"x":662,"y":252},{"time":11648,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":11836,"x":662,"y":252}],"scrollY":0,"scrollX":0,"timestamp":1639968125686}] \ No newline at end of file diff --git a/test/runTest/actions/graphic-animation.json b/test/runTest/actions/graphic-animation.json new file mode 100644 index 0000000000..68b00e7a1b --- /dev/null +++ b/test/runTest/actions/graphic-animation.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousedown","time":247,"x":77,"y":90},{"type":"mouseup","time":393,"x":77,"y":90},{"time":394,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":2848,"x":77,"y":90},{"type":"mouseup","time":2931,"x":77,"y":90},{"time":2932,"delay":400,"type":"screenshot-auto"}],"scrollY":443,"scrollX":0,"timestamp":1639968073824}] \ No newline at end of file diff --git a/test/runTest/actions/graphic-transition.json b/test/runTest/actions/graphic-transition.json new file mode 100644 index 0000000000..6f53438322 --- /dev/null +++ b/test/runTest/actions/graphic-transition.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousedown","time":438,"x":72,"y":82},{"type":"mouseup","time":558,"x":72,"y":82},{"time":559,"delay":0,"type":"screenshot-auto"},{"type":"mousemove","time":845,"x":72,"y":82},{"type":"mousemove","time":1049,"x":123,"y":84},{"type":"mousemove","time":1261,"x":134,"y":84},{"type":"mousedown","time":1407,"x":134,"y":84},{"type":"mouseup","time":1491,"x":134,"y":84},{"time":1492,"delay":0,"type":"screenshot-auto"},{"type":"mousemove","time":1628,"x":134,"y":84},{"type":"mousemove","time":1828,"x":230,"y":79},{"type":"mousemove","time":2028,"x":264,"y":78},{"type":"mousemove","time":2232,"x":266,"y":78},{"type":"mousedown","time":2307,"x":266,"y":78},{"type":"mousemove","time":2395,"x":266,"y":79},{"type":"mouseup","time":2416,"x":266,"y":79},{"time":2417,"delay":0,"type":"screenshot-auto"},{"type":"mousemove","time":2600,"x":266,"y":79},{"type":"mousemove","time":2812,"x":338,"y":83},{"type":"mousemove","time":3015,"x":369,"y":81},{"type":"mousedown","time":3233,"x":389,"y":78},{"type":"mousemove","time":3237,"x":389,"y":78},{"type":"mouseup","time":3316,"x":389,"y":78},{"time":3317,"delay":0,"type":"screenshot-auto"},{"type":"mousemove","time":3512,"x":389,"y":78},{"type":"mousemove","time":3712,"x":490,"y":84},{"type":"mousemove","time":3916,"x":521,"y":84},{"type":"mousedown","time":4101,"x":529,"y":83},{"type":"mousemove","time":4132,"x":529,"y":83},{"type":"mouseup","time":4190,"x":529,"y":83},{"time":4191,"delay":0,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1639968192492},{"name":"Action 2","ops":[{"type":"mousedown","time":330,"x":66,"y":21},{"type":"mouseup","time":450,"x":66,"y":21},{"time":451,"delay":0,"type":"screenshot-auto"},{"type":"mousedown","time":1231,"x":66,"y":21},{"type":"mouseup","time":1349,"x":66,"y":21},{"time":1350,"delay":0,"type":"screenshot-auto"},{"type":"mousedown","time":2323,"x":66,"y":21},{"type":"mouseup","time":2431,"x":66,"y":21},{"time":2432,"delay":0,"type":"screenshot-auto"},{"type":"mousedown","time":3255,"x":66,"y":21},{"type":"mouseup","time":3341,"x":66,"y":21},{"time":3342,"delay":0,"type":"screenshot-auto"}],"scrollY":557.8229370117188,"scrollX":0,"timestamp":1639968240514},{"name":"Action 3","ops":[{"type":"mousedown","time":393,"x":38,"y":232},{"type":"mouseup","time":469,"x":38,"y":232},{"time":470,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":820,"x":39,"y":232},{"type":"mousemove","time":1021,"x":59,"y":230},{"type":"mousemove","time":1226,"x":92,"y":228},{"type":"mousemove","time":1444,"x":95,"y":227},{"type":"mousedown","time":1513,"x":95,"y":227},{"type":"mouseup","time":1609,"x":95,"y":227},{"time":1610,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":2419,"x":95,"y":227},{"type":"mouseup","time":2510,"x":95,"y":227},{"time":2511,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":3393,"x":95,"y":227},{"type":"mouseup","time":3477,"x":95,"y":227},{"time":3478,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3688,"x":96,"y":227},{"type":"mousemove","time":3888,"x":154,"y":230},{"type":"mousemove","time":4088,"x":179,"y":228},{"type":"mousemove","time":4293,"x":179,"y":228},{"type":"mousedown","time":4394,"x":179,"y":228},{"type":"mouseup","time":4476,"x":179,"y":228},{"time":4477,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4671,"x":179,"y":228},{"type":"mousemove","time":4878,"x":175,"y":229},{"type":"mousemove","time":5088,"x":64,"y":231},{"type":"mousemove","time":5288,"x":29,"y":228},{"type":"mousedown","time":5327,"x":29,"y":228},{"type":"mouseup","time":5410,"x":29,"y":228},{"time":5411,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5495,"x":29,"y":228},{"type":"mousemove","time":5955,"x":30,"y":228},{"type":"mousemove","time":6155,"x":76,"y":228},{"type":"mousedown","time":6329,"x":92,"y":228},{"type":"mousemove","time":6360,"x":92,"y":228},{"type":"mouseup","time":6427,"x":92,"y":228},{"time":6428,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6655,"x":92,"y":229},{"type":"mousemove","time":6855,"x":54,"y":232},{"type":"mousemove","time":7055,"x":41,"y":232},{"type":"mousedown","time":7166,"x":40,"y":232},{"type":"mouseup","time":7252,"x":40,"y":232},{"time":7253,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7266,"x":40,"y":232},{"type":"mousemove","time":7721,"x":40,"y":232},{"type":"mousemove","time":7921,"x":155,"y":235},{"type":"mousemove","time":8127,"x":176,"y":232},{"type":"mousedown","time":8162,"x":176,"y":232},{"type":"mouseup","time":8227,"x":176,"y":232},{"time":8228,"delay":400,"type":"screenshot-auto"}],"scrollY":847.049560546875,"scrollX":0,"timestamp":1639968411013}] \ No newline at end of file diff --git a/test/ut/spec/component/graphic/setOption.test.ts b/test/ut/spec/component/graphic/setOption.test.ts index 4669fa00d3..e26d66b700 100755 --- a/test/ut/spec/component/graphic/setOption.test.ts +++ b/test/ut/spec/component/graphic/setOption.test.ts @@ -23,8 +23,9 @@ import { EChartsType } from '../../../../../src/echarts'; import Element from 'zrender/src/Element'; import { EChartsOption } from '../../../../../src/export/option'; import { - GraphicComponentOption, GraphicComponentImageOption -} from '../../../../../src/component/graphic/install'; + GraphicComponentOption, + GraphicComponentImageOption +} from '../../../../../src/component/graphic/GraphicModel'; import Group from 'zrender/src/graphic/Group'; import { Dictionary } from 'zrender/src/core/types';