Spaces:
Running
Running
| /* | |
| * 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 { | |
| hasOwn, assert, isString, retrieve2, retrieve3, defaults, each, indexOf | |
| } from 'zrender/src/core/util'; | |
| import * as graphicUtil from '../../util/graphic'; | |
| import { setDefaultStateProxy, toggleHoverEmphasis } from '../../util/states'; | |
| import * as labelStyleHelper from '../../label/labelStyle'; | |
| import {getDefaultLabel} from '../helper/labelHelper'; | |
| import {getLayoutOnAxis} from '../../layout/barGrid'; | |
| import DataDiffer from '../../data/DataDiffer'; | |
| import Model from '../../model/Model'; | |
| import ChartView from '../../view/Chart'; | |
| import {createClipPath} from '../helper/createClipPathFromCoordSys'; | |
| import { | |
| EventQueryItem, ECActionEvent, | |
| DimensionLoose, | |
| ParsedValue, | |
| Dictionary, | |
| Payload, | |
| StageHandlerProgressParams, | |
| ViewRootGroup, | |
| ZRStyleProps, | |
| DisplayState, | |
| ECElement, | |
| DisplayStateNonNormal, | |
| OrdinalRawValue, | |
| InnerDecalObject | |
| } from '../../util/types'; | |
| 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'; | |
| import preparePolar from '../../coord/polar/prepareCustom'; | |
| import prepareCalendar from '../../coord/calendar/prepareCustom'; | |
| import SeriesData, { DefaultDataVisual } from '../../data/SeriesData'; | |
| import GlobalModel from '../../model/Global'; | |
| import ExtensionAPI from '../../core/ExtensionAPI'; | |
| 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'; | |
| import { TextStyleProps } from 'zrender/src/graphic/Text'; | |
| import { | |
| convertToEC4StyleForCustomSerise, | |
| isEC4CompatibleStyle, | |
| convertFromEC4CompatibleStyle, | |
| LegacyStyleProps, | |
| warnDeprecated | |
| } from '../../util/styleCompat'; | |
| import { ItemStyleProps } from '../../model/mixin/itemStyle'; | |
| import { throwError } from '../../util/log'; | |
| import { createOrUpdatePatternFromDecal } from '../../util/decal'; | |
| import CustomSeriesModel, { | |
| CustomImageOption, | |
| CustomElementOption, | |
| CustomElementOptionOnState, | |
| CustomSVGPathOption, | |
| CustomBaseZRPathOption, | |
| CustomDisplayableOption, | |
| CustomSeriesRenderItemAPI, | |
| CustomSeriesRenderItemParams, | |
| CustomGroupOption, | |
| WrapEncodeDefRet, | |
| NonStyleVisualProps, | |
| StyleVisualProps, | |
| STYLE_VISUAL_TYPE, | |
| NON_STYLE_VISUAL_PROPS, | |
| customInnerStore, | |
| PrepareCustomInfo, | |
| CustomPathOption, | |
| CustomRootElementOption, | |
| CustomSeriesOption | |
| } from './CustomSeries'; | |
| import { PatternObject } from 'zrender/src/graphic/Pattern'; | |
| import { | |
| applyLeaveTransition, | |
| applyUpdateTransition, | |
| ElementRootTransitionProp | |
| } from '../../animation/customGraphicTransition'; | |
| import { | |
| applyKeyframeAnimation, | |
| stopPreviousKeyframeAnimationAndRestore | |
| } from '../../animation/customGraphicKeyframeAnimation'; | |
| import type SeriesModel from '../../model/Series'; | |
| const EMPHASIS = 'emphasis' as const; | |
| const NORMAL = 'normal' as const; | |
| const BLUR = 'blur' as const; | |
| const SELECT = 'select' as const; | |
| const STATES = [NORMAL, EMPHASIS, BLUR, SELECT] as const; | |
| const PATH_ITEM_STYLE = { | |
| normal: ['itemStyle'], | |
| emphasis: [EMPHASIS, 'itemStyle'], | |
| blur: [BLUR, 'itemStyle'], | |
| select: [SELECT, 'itemStyle'] | |
| } as const; | |
| const PATH_LABEL = { | |
| normal: ['label'], | |
| emphasis: [EMPHASIS, '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'; | |
| type AttachedTxInfo = { | |
| isLegacy: boolean; | |
| normal: { | |
| cfg: ElementTextConfig; | |
| conOpt: CustomElementOption | false; | |
| }; | |
| emphasis: { | |
| cfg: ElementTextConfig; | |
| conOpt: CustomElementOptionOnState; | |
| }; | |
| blur: { | |
| cfg: ElementTextConfig; | |
| conOpt: CustomElementOptionOnState; | |
| }; | |
| select: { | |
| cfg: ElementTextConfig; | |
| conOpt: CustomElementOptionOnState; | |
| }; | |
| }; | |
| const attachedTxInfoTmp = { | |
| normal: {}, | |
| emphasis: {}, | |
| blur: {}, | |
| select: {} | |
| } as AttachedTxInfo; | |
| /** | |
| * To reduce total package size of each coordinate systems, the modules `prepareCustom` | |
| * of each coordinate systems are not required by each coordinate systems directly, but | |
| * required by the module `custom`. | |
| * | |
| * prepareInfoForCustomSeries {Function}: optional | |
| * @return {Object} {coordSys: {...}, api: { | |
| * coord: function (data, clamp) {}, // return point in global. | |
| * size: function (dataSize, dataItem) {} // return size of each axis in coordSys. | |
| * }} | |
| */ | |
| const prepareCustoms: Dictionary<PrepareCustomInfo> = { | |
| cartesian2d: prepareCartesian2d, | |
| geo: prepareGeo, | |
| single: prepareSingleAxis, | |
| polar: preparePolar, | |
| calendar: prepareCalendar | |
| }; | |
| function isPath(el: Element): el is graphicUtil.Path { | |
| return el instanceof graphicUtil.Path; | |
| } | |
| function isDisplayable(el: Element) : el is Displayable { | |
| return el instanceof Displayable; | |
| } | |
| function copyElement(sourceEl: Element, targetEl: Element) { | |
| targetEl.copyTransform(sourceEl); | |
| if (isDisplayable(targetEl) && isDisplayable(sourceEl)) { | |
| targetEl.setStyle(sourceEl.style); | |
| targetEl.z = sourceEl.z; | |
| targetEl.z2 = sourceEl.z2; | |
| targetEl.zlevel = sourceEl.zlevel; | |
| targetEl.invisible = sourceEl.invisible; | |
| targetEl.ignore = sourceEl.ignore; | |
| if (isPath(targetEl) && isPath(sourceEl)) { | |
| targetEl.setShape(sourceEl.shape); | |
| } | |
| } | |
| } | |
| export default class CustomChartView extends ChartView { | |
| static type = 'custom'; | |
| readonly type = CustomChartView.type; | |
| private _data: SeriesData; | |
| private _progressiveEls: Element[]; | |
| render( | |
| customSeries: CustomSeriesModel, | |
| ecModel: GlobalModel, | |
| api: ExtensionAPI, | |
| payload: Payload | |
| ): void { | |
| // Clear previously rendered progressive elements. | |
| this._progressiveEls = null; | |
| const oldData = this._data; | |
| const data = customSeries.getData(); | |
| const group = this.group; | |
| const renderItem = makeRenderItem(customSeries, data, ecModel, api); | |
| if (!oldData) { | |
| // Previous render is incremental render or first render. | |
| // Needs remove the incremental rendered elements. | |
| group.removeAll(); | |
| } | |
| data.diff(oldData) | |
| .add(function (newIdx) { | |
| createOrUpdateItem( | |
| api, null, newIdx, renderItem(newIdx, payload), customSeries, group, | |
| data | |
| ); | |
| }) | |
| .remove(function (oldIdx) { | |
| const el = oldData.getItemGraphicEl(oldIdx); | |
| el && applyLeaveTransition(el, customInnerStore(el).option, customSeries); | |
| }) | |
| .update(function (newIdx, oldIdx) { | |
| const oldEl = oldData.getItemGraphicEl(oldIdx); | |
| createOrUpdateItem( | |
| api, oldEl, newIdx, renderItem(newIdx, payload), customSeries, group, | |
| data | |
| ); | |
| }) | |
| .execute(); | |
| // Do clipping | |
| const clipPath = customSeries.get('clip', true) | |
| ? createClipPath(customSeries.coordinateSystem, false, customSeries) | |
| : null; | |
| if (clipPath) { | |
| group.setClipPath(clipPath); | |
| } | |
| else { | |
| group.removeClipPath(); | |
| } | |
| this._data = data; | |
| } | |
| incrementalPrepareRender( | |
| customSeries: CustomSeriesModel, | |
| ecModel: GlobalModel, | |
| api: ExtensionAPI | |
| ): void { | |
| this.group.removeAll(); | |
| this._data = null; | |
| } | |
| incrementalRender( | |
| params: StageHandlerProgressParams, | |
| customSeries: CustomSeriesModel, | |
| ecModel: GlobalModel, | |
| api: ExtensionAPI, | |
| payload: Payload | |
| ): void { | |
| const data = customSeries.getData(); | |
| const renderItem = makeRenderItem(customSeries, data, ecModel, api); | |
| const progressiveEls: Element[] = this._progressiveEls = []; | |
| function setIncrementalAndHoverLayer(el: Displayable) { | |
| if (!el.isGroup) { | |
| el.incremental = true; | |
| el.ensureState('emphasis').hoverLayer = true; | |
| } | |
| } | |
| for (let idx = params.start; idx < params.end; idx++) { | |
| const el = createOrUpdateItem( | |
| null, null, idx, renderItem(idx, payload), customSeries, this.group, data | |
| ); | |
| if (el) { | |
| el.traverse(setIncrementalAndHoverLayer); | |
| progressiveEls.push(el); | |
| } | |
| } | |
| } | |
| eachRendered(cb: (el: Element) => boolean | void) { | |
| graphicUtil.traverseElements(this._progressiveEls || this.group, cb); | |
| } | |
| filterForExposedEvent( | |
| eventType: string, query: EventQueryItem, targetEl: Element, packedEvent: ECActionEvent | |
| ): boolean { | |
| const elementName = query.element; | |
| if (elementName == null || targetEl.name === elementName) { | |
| return true; | |
| } | |
| // Enable to give a name on a group made by `renderItem`, and listen | |
| // events that are triggered by its descendents. | |
| while ((targetEl = (targetEl.__hostTarget || targetEl.parent)) && targetEl !== this.group) { | |
| if (targetEl.name === elementName) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| } | |
| function createEl(elOption: CustomElementOption): Element { | |
| const graphicType = elOption.type; | |
| let el; | |
| // Those graphic elements are not shapes. They should not be | |
| // overwritten by users, so do them first. | |
| if (graphicType === 'path') { | |
| const shape = (elOption as CustomSVGPathOption).shape; | |
| // Using pathRect brings convenience to users sacle svg path. | |
| const pathRect = (shape.width != null && shape.height != null) | |
| ? { | |
| x: shape.x || 0, | |
| y: shape.y || 0, | |
| width: shape.width, | |
| height: shape.height | |
| } as RectLike | |
| : null; | |
| const pathData = getPathData(shape); | |
| // Path is also used for icon, so layout 'center' by default. | |
| el = graphicUtil.makePath(pathData, null, pathRect, shape.layout || 'center'); | |
| customInnerStore(el).customPathData = pathData; | |
| } | |
| else if (graphicType === 'image') { | |
| el = new graphicUtil.Image({}); | |
| customInnerStore(el).customImagePath = (elOption as CustomImageOption).style.image; | |
| } | |
| else if (graphicType === 'text') { | |
| el = new graphicUtil.Text({}); | |
| // customInnerStore(el).customText = (elOption.style as TextStyleProps).text; | |
| } | |
| else if (graphicType === 'group') { | |
| el = new graphicUtil.Group(); | |
| } | |
| else if (graphicType === 'compoundPath') { | |
| throw new Error('"compoundPath" is not supported yet.'); | |
| } | |
| else { | |
| const Clz = graphicUtil.getShapeClass(graphicType); | |
| if (!Clz) { | |
| let errMsg = ''; | |
| if (__DEV__) { | |
| errMsg = 'graphic type "' + graphicType + '" can not be found.'; | |
| } | |
| throwError(errMsg); | |
| } | |
| el = new Clz(); | |
| } | |
| customInnerStore(el).customGraphicType = graphicType; | |
| el.name = elOption.name; | |
| // Compat ec4: the default z2 lift is 1. If changing the number, | |
| // some cases probably be broken: hierarchy layout along z, like circle packing, | |
| // where emphasis only intending to modify color/border rather than lift z2. | |
| (el as ECElement).z2EmphasisLift = 1; | |
| (el as ECElement).z2SelectLift = 1; | |
| return el; | |
| } | |
| /** | |
| * ---------------------------------------------------------- | |
| * [STRATEGY_MERGE] Merge properties or erase all properties: | |
| * | |
| * Based on the fact that the existing zr element probably is reused, we now consider whether | |
| * merge or erase all properties to the existing elements. | |
| * That is, if a certain props is not specified in the latest return of `renderItem`: | |
| * + "Merge" means that do not modify the value on the existing element. | |
| * + "Erase all" means that use a default value to the existing element. | |
| * | |
| * "Merge" might bring some unexpected state retaining for users and "erase all" seems to be | |
| * more safe. "erase all" forces users to specify all of the props each time, which is recommended | |
| * in most cases. | |
| * But "erase all" theoretically disables the chance of performance optimization (e.g., just | |
| * generete shape and style at the first time rather than always do that). | |
| * So we still use "merge" rather than "erase all". If users need "erase all", they can | |
| * simply always set all of the props each time. | |
| * Some "object-like" config like `textConfig`, `textContent`, `style` which are not needed for | |
| * every element, so we replace them only when users specify them. And that is a total replace. | |
| * | |
| * TODO: There is no hint of 'isFirst' to users. So the performance enhancement cannot be | |
| * performed yet. Consider the case: | |
| * (1) setOption to "mergeChildren" with a smaller children count | |
| * (2) Use dataZoom to make an item disappear. | |
| * (3) User dataZoom to make the item display again. At that time, renderItem need to return the | |
| * full option rather than partial option to recreate the element. | |
| * | |
| * ---------------------------------------------- | |
| * [STRATEGY_NULL] `hasOwnProperty` or `== null`: | |
| * | |
| * Ditinguishing "own property" probably bring little trouble to user when make el options. | |
| * So we trade a {xx: null} or {xx: undefined} as "not specified" if possible rather than | |
| * "set them to null/undefined". In most cases, props can not be cleared. Some typicall | |
| * clearable props like `style`/`textConfig`/`textContent` we enable `false` to means | |
| * "clear". In some other special cases that the prop is able to set as null/undefined, | |
| * but not suitable to use `false`, `hasOwnProperty` is checked. | |
| * | |
| * --------------------------------------------- | |
| * [STRATEGY_TRANSITION] The rule of transition: | |
| * + For props on the root level of a element: | |
| * If there is no `transition` specified, tansform props will be transitioned by default, | |
| * which is the same as the previous setting in echarts4 and suitable for the scenario | |
| * of dataZoom change. | |
| * If `transition` specified, only the specified props will be transitioned. | |
| * + For props in `shape` and `style`: | |
| * Only props specified in `transition` will be transitioned. | |
| * + Break: | |
| * Since ec5, do not make transition to shape by default, because it might result in | |
| * performance issue (especially `points` of polygon) and do not necessary in most cases. | |
| * | |
| * @return if `isMorphTo`, return `allPropsFinal`. | |
| */ | |
| interface InnerCustomZRPathOptionStyle extends PathStyleProps { | |
| __decalPattern: PatternObject | |
| } | |
| function updateElNormal( | |
| // Can be null/undefined | |
| api: ExtensionAPI, | |
| el: Element, | |
| dataIndex: number, | |
| elOption: CustomElementOption, | |
| attachedTxInfo: AttachedTxInfo, | |
| seriesModel: CustomSeriesModel, | |
| 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? | |
| // TODO:5.0 textConfig transition animation? | |
| 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; | |
| if (styleOpt) { | |
| if (el.type === 'text') { | |
| const textOptionStyle = styleOpt as TextStyleProps; | |
| // Compatible with ec4: if `textFill` or `textStroke` exists use them. | |
| hasOwn(textOptionStyle, 'textFill') && ( | |
| textOptionStyle.fill = (textOptionStyle as any).textFill | |
| ); | |
| hasOwn(textOptionStyle, 'textStroke') && ( | |
| textOptionStyle.stroke = (textOptionStyle as any).textStroke | |
| ); | |
| } | |
| let decalPattern; | |
| const decalObj = isPath(el) ? (styleOpt as CustomBaseZRPathOption['style']).decal : null; | |
| if (api && decalObj) { | |
| (decalObj as InnerDecalObject).dirty = true; | |
| decalPattern = createOrUpdatePatternFromDecal(decalObj, api); | |
| } | |
| // Always overwrite in case user specify this prop. | |
| (styleOpt as InnerCustomZRPathOptionStyle).__decalPattern = decalPattern; | |
| } | |
| if (isDisplayable(el)) { | |
| if (styleOpt) { | |
| const decalPattern = (styleOpt as InnerCustomZRPathOptionStyle).__decalPattern; | |
| if (decalPattern) { | |
| (styleOpt as PathStyleProps).decal = decalPattern; | |
| } | |
| } | |
| } | |
| applyUpdateTransition(el, elOption, seriesModel, { | |
| dataIndex, | |
| isInit, | |
| clearStyle: true | |
| }); | |
| applyKeyframeAnimation(el, elOption.keyframeAnimation, seriesModel); | |
| } | |
| function updateElOnState( | |
| state: DisplayStateNonNormal, | |
| el: Element, | |
| elStateOpt: CustomElementOptionOnState, | |
| styleOpt: CustomElementOptionOnState['style'], | |
| attachedTxInfo: AttachedTxInfo | |
| ): void { | |
| const elDisplayable = el.isGroup ? null : el as Displayable; | |
| const txCfgOpt = attachedTxInfo && attachedTxInfo[state].cfg; | |
| // PENDING:5.0 support customize scale change and transition animation? | |
| if (elDisplayable) { | |
| // By default support auto lift color when hover whether `emphasis` specified. | |
| const stateObj = elDisplayable.ensureState(state); | |
| if (styleOpt === false) { | |
| const existingEmphasisState = elDisplayable.getState(state); | |
| if (existingEmphasisState) { | |
| existingEmphasisState.style = null; | |
| } | |
| } | |
| else { | |
| // style is needed to enable default emphasis. | |
| stateObj.style = styleOpt || null; | |
| } | |
| // If `elOption.styleEmphasis` or `elOption.emphasis.style` is `false`, | |
| // remove hover style. | |
| // If `elOption.textConfig` or `elOption.emphasis.textConfig` is null/undefined, it does not | |
| // make sense. So for simplicity, we do not ditinguish `hasOwnProperty` and null/undefined. | |
| if (txCfgOpt) { | |
| stateObj.textConfig = txCfgOpt; | |
| } | |
| setDefaultStateProxy(elDisplayable); | |
| } | |
| } | |
| function updateZ( | |
| el: Element, | |
| elOption: CustomElementOption, | |
| seriesModel: CustomSeriesModel | |
| ): void { | |
| // Group not support textContent and not support z yet. | |
| if (el.isGroup) { | |
| return; | |
| } | |
| const elDisplayable = el as Displayable; | |
| const currentZ = seriesModel.currentZ; | |
| const currentZLevel = seriesModel.currentZLevel; | |
| // Always erase. | |
| elDisplayable.z = currentZ; | |
| elDisplayable.zlevel = currentZLevel; | |
| // z2 must not be null/undefined, otherwise sort error may occur. | |
| const optZ2 = (elOption as CustomDisplayableOption).z2; | |
| optZ2 != null && (elDisplayable.z2 = optZ2 || 0); | |
| for (let i = 0; i < STATES.length; i++) { | |
| updateZForEachState(elDisplayable, elOption, STATES[i]); | |
| } | |
| } | |
| function updateZForEachState( | |
| elDisplayable: Displayable, | |
| elOption: CustomDisplayableOption, | |
| state: DisplayState | |
| ): void { | |
| const isNormal = state === NORMAL; | |
| const elStateOpt = isNormal ? elOption : retrieveStateOption( | |
| elOption as CustomElementOption, | |
| state as DisplayStateNonNormal | |
| ); | |
| const optZ2 = elStateOpt ? elStateOpt.z2 : null; | |
| let stateObj; | |
| if (optZ2 != null) { | |
| // Do not `ensureState` until required. | |
| stateObj = isNormal ? elDisplayable : elDisplayable.ensureState(state); | |
| stateObj.z2 = optZ2 || 0; | |
| } | |
| } | |
| function makeRenderItem( | |
| customSeries: CustomSeriesModel, | |
| data: SeriesData<CustomSeriesModel>, | |
| ecModel: GlobalModel, | |
| api: ExtensionAPI | |
| ) { | |
| const renderItem = customSeries.get('renderItem'); | |
| const coordSys = customSeries.coordinateSystem; | |
| let prepareResult = {} as ReturnType<PrepareCustomInfo>; | |
| if (coordSys) { | |
| if (__DEV__) { | |
| assert(renderItem, 'series.render is required.'); | |
| assert( | |
| coordSys.prepareCustoms || prepareCustoms[coordSys.type], | |
| 'This coordSys does not support custom series.' | |
| ); | |
| } | |
| // `coordSys.prepareCustoms` is used for external coord sys like bmap. | |
| prepareResult = coordSys.prepareCustoms | |
| ? coordSys.prepareCustoms(coordSys) | |
| : prepareCustoms[coordSys.type](coordSys); | |
| } | |
| const userAPI = defaults({ | |
| getWidth: api.getWidth, | |
| getHeight: api.getHeight, | |
| getZr: api.getZr, | |
| getDevicePixelRatio: api.getDevicePixelRatio, | |
| value: value, | |
| style: style, | |
| ordinalRawValue: ordinalRawValue, | |
| styleEmphasis: styleEmphasis, | |
| visual: visual, | |
| barLayout: barLayout, | |
| currentSeriesIndices: currentSeriesIndices, | |
| font: font | |
| }, prepareResult.api || {}) as CustomSeriesRenderItemAPI; | |
| const userParams: CustomSeriesRenderItemParams = { | |
| // The life cycle of context: current round of rendering. | |
| // The global life cycle is probably not necessary, because | |
| // user can store global status by themselves. | |
| context: {}, | |
| seriesId: customSeries.id, | |
| seriesName: customSeries.name, | |
| seriesIndex: customSeries.seriesIndex, | |
| coordSys: prepareResult.coordSys, | |
| dataInsideLength: data.count(), | |
| encode: wrapEncodeDef(customSeries.getData()) | |
| } as CustomSeriesRenderItemParams; | |
| // If someday intending to refactor them to a class, should consider do not | |
| // break change: currently these attribute member are encapsulated in a closure | |
| // so that do not need to force user to call these method with a scope. | |
| // Do not support call `api` asynchronously without dataIndexInside input. | |
| let currDataIndexInside: number; | |
| let currItemModel: Model<CustomSeriesOption>; | |
| let currItemStyleModels: Partial<Record<DisplayState, Model<CustomSeriesOption['itemStyle']>>> = {}; | |
| let currLabelModels: Partial<Record<DisplayState, Model<CustomSeriesOption['label']>>> = {}; | |
| const seriesItemStyleModels = {} as Record<DisplayState, Model<CustomSeriesOption['itemStyle']>>; | |
| const seriesLabelModels = {} as Record<DisplayState, Model<CustomSeriesOption['label']>>; | |
| for (let i = 0; i < STATES.length; i++) { | |
| const stateName = STATES[i]; | |
| seriesItemStyleModels[stateName] = (customSeries as Model<CustomSeriesOption>) | |
| .getModel(PATH_ITEM_STYLE[stateName]); | |
| seriesLabelModels[stateName] = (customSeries as Model<CustomSeriesOption>) | |
| .getModel(PATH_LABEL[stateName]); | |
| } | |
| function getItemModel(dataIndexInside: number): Model<CustomSeriesOption> { | |
| return dataIndexInside === currDataIndexInside | |
| ? (currItemModel || (currItemModel = data.getItemModel(dataIndexInside))) | |
| : data.getItemModel(dataIndexInside); | |
| } | |
| function getItemStyleModel(dataIndexInside: number, state: DisplayState) { | |
| return !data.hasItemOption | |
| ? seriesItemStyleModels[state] | |
| : dataIndexInside === currDataIndexInside | |
| ? (currItemStyleModels[state] || ( | |
| currItemStyleModels[state] = getItemModel(dataIndexInside).getModel(PATH_ITEM_STYLE[state]) | |
| )) | |
| : getItemModel(dataIndexInside).getModel(PATH_ITEM_STYLE[state]); | |
| } | |
| function getLabelModel(dataIndexInside: number, state: DisplayState) { | |
| return !data.hasItemOption | |
| ? seriesLabelModels[state] | |
| : dataIndexInside === currDataIndexInside | |
| ? (currLabelModels[state] || ( | |
| currLabelModels[state] = getItemModel(dataIndexInside).getModel(PATH_LABEL[state]) | |
| )) | |
| : getItemModel(dataIndexInside).getModel(PATH_LABEL[state]); | |
| } | |
| return function (dataIndexInside: number, payload: Payload): CustomElementOption { | |
| currDataIndexInside = dataIndexInside; | |
| currItemModel = null; | |
| currItemStyleModels = {}; | |
| currLabelModels = {}; | |
| return renderItem && renderItem( | |
| defaults({ | |
| dataIndexInside: dataIndexInside, | |
| dataIndex: data.getRawIndex(dataIndexInside), | |
| // Can be used for optimization when zoom or roam. | |
| actionType: payload ? payload.type : null | |
| } as CustomSeriesRenderItemParams, userParams), | |
| userAPI | |
| ); | |
| }; | |
| /** | |
| * @public | |
| * @param dim by default 0. | |
| * @param dataIndexInside by default `currDataIndexInside`. | |
| */ | |
| function value(dim?: DimensionLoose, dataIndexInside?: number): ParsedValue { | |
| dataIndexInside == null && (dataIndexInside = currDataIndexInside); | |
| return data.getStore().get(data.getDimensionIndex(dim || 0), dataIndexInside); | |
| } | |
| /** | |
| * @public | |
| * @param dim by default 0. | |
| * @param dataIndexInside by default `currDataIndexInside`. | |
| */ | |
| function ordinalRawValue(dim?: DimensionLoose, dataIndexInside?: number): ParsedValue | OrdinalRawValue { | |
| dataIndexInside == null && (dataIndexInside = currDataIndexInside); | |
| dim = dim || 0; | |
| const dimInfo = data.getDimensionInfo(dim); | |
| if (!dimInfo) { | |
| const dimIndex = data.getDimensionIndex(dim); | |
| return dimIndex >= 0 ? data.getStore().get(dimIndex, dataIndexInside) : undefined; | |
| } | |
| const val = data.get(dimInfo.name, dataIndexInside); | |
| const ordinalMeta = dimInfo && dimInfo.ordinalMeta; | |
| return ordinalMeta | |
| ? ordinalMeta.categories[val as number] | |
| : val; | |
| } | |
| /** | |
| * @deprecated The original intention of `api.style` is enable to set itemStyle | |
| * like other series. But it is not necessary and not easy to give a strict definition | |
| * of what it returns. And since echarts5 it needs to be make compat work. So | |
| * deprecates it since echarts5. | |
| * | |
| * By default, `visual` is applied to style (to support visualMap). | |
| * `visual.color` is applied at `fill`. If user want apply visual.color on `stroke`, | |
| * it can be implemented as: | |
| * `api.style({stroke: api.visual('color'), fill: null})`; | |
| * | |
| * [Compat]: since ec5, RectText has been separated from its hosts el. | |
| * so `api.style()` will only return the style from `itemStyle` but not handle `label` | |
| * any more. But `series.label` config is never published in doc. | |
| * We still compat it in `api.style()`. But not encourage to use it and will still not | |
| * to pulish it to doc. | |
| * @public | |
| * @param dataIndexInside by default `currDataIndexInside`. | |
| */ | |
| function style(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps { | |
| if (__DEV__) { | |
| warnDeprecated('api.style', 'Please write literal style directly instead.'); | |
| } | |
| dataIndexInside == null && (dataIndexInside = currDataIndexInside); | |
| const style = data.getItemVisual(dataIndexInside, 'style'); | |
| const visualColor = style && style.fill; | |
| const opacity = style && style.opacity; | |
| let itemStyle = getItemStyleModel(dataIndexInside, NORMAL).getItemStyle(); | |
| visualColor != null && (itemStyle.fill = visualColor); | |
| opacity != null && (itemStyle.opacity = opacity); | |
| const opt = {inheritColor: isString(visualColor) ? visualColor : '#000'}; | |
| const labelModel = getLabelModel(dataIndexInside, NORMAL); | |
| // Now that the feature of "auto adjust text fill/stroke" has been migrated to zrender | |
| // since ec5, we should set `isAttached` as `false` here and make compat in | |
| // `convertToEC4StyleForCustomSerise`. | |
| const textStyle = labelStyleHelper.createTextStyle(labelModel, null, opt, false, true); | |
| textStyle.text = labelModel.getShallow('show') | |
| ? retrieve2( | |
| customSeries.getFormattedLabel(dataIndexInside, NORMAL), | |
| getDefaultLabel(data, dataIndexInside) | |
| ) | |
| : null; | |
| const textConfig = labelStyleHelper.createTextConfig(labelModel, opt, false); | |
| preFetchFromExtra(userProps, itemStyle); | |
| itemStyle = convertToEC4StyleForCustomSerise(itemStyle, textStyle, textConfig); | |
| userProps && applyUserPropsAfter(itemStyle, userProps); | |
| (itemStyle as LegacyStyleProps).legacy = true; | |
| return itemStyle; | |
| } | |
| /** | |
| * @deprecated The reason see `api.style()` | |
| * @public | |
| * @param dataIndexInside by default `currDataIndexInside`. | |
| */ | |
| function styleEmphasis(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps { | |
| if (__DEV__) { | |
| warnDeprecated('api.styleEmphasis', 'Please write literal style directly instead.'); | |
| } | |
| dataIndexInside == null && (dataIndexInside = currDataIndexInside); | |
| let itemStyle = getItemStyleModel(dataIndexInside, EMPHASIS).getItemStyle(); | |
| const labelModel = getLabelModel(dataIndexInside, EMPHASIS); | |
| const textStyle = labelStyleHelper.createTextStyle(labelModel, null, null, true, true); | |
| textStyle.text = labelModel.getShallow('show') | |
| ? retrieve3( | |
| customSeries.getFormattedLabel(dataIndexInside, EMPHASIS), | |
| customSeries.getFormattedLabel(dataIndexInside, NORMAL), | |
| getDefaultLabel(data, dataIndexInside) | |
| ) | |
| : null; | |
| const textConfig = labelStyleHelper.createTextConfig(labelModel, null, true); | |
| preFetchFromExtra(userProps, itemStyle); | |
| itemStyle = convertToEC4StyleForCustomSerise(itemStyle, textStyle, textConfig); | |
| userProps && applyUserPropsAfter(itemStyle, userProps); | |
| (itemStyle as LegacyStyleProps).legacy = true; | |
| return itemStyle; | |
| } | |
| function applyUserPropsAfter(itemStyle: ZRStyleProps, extra: ZRStyleProps): void { | |
| for (const key in extra) { | |
| if (hasOwn(extra, key)) { | |
| (itemStyle as any)[key] = (extra as any)[key]; | |
| } | |
| } | |
| } | |
| function preFetchFromExtra(extra: ZRStyleProps, itemStyle: ItemStyleProps): void { | |
| // A trick to retrieve those props firstly, which are used to | |
| // apply auto inside fill/stroke in `convertToEC4StyleForCustomSerise`. | |
| // (It's not reasonable but only for a degree of compat) | |
| if (extra) { | |
| (extra as any).textFill && ((itemStyle as any).textFill = (extra as any).textFill); | |
| (extra as any).textPosition && ((itemStyle as any).textPosition = (extra as any).textPosition); | |
| } | |
| } | |
| /** | |
| * @public | |
| * @param dataIndexInside by default `currDataIndexInside`. | |
| */ | |
| function visual<VT extends NonStyleVisualProps | StyleVisualProps>( | |
| visualType: VT, | |
| dataIndexInside?: number | |
| ): VT extends NonStyleVisualProps ? DefaultDataVisual[VT] | |
| : VT extends StyleVisualProps ? PathStyleProps[typeof STYLE_VISUAL_TYPE[VT]] | |
| : never { | |
| dataIndexInside == null && (dataIndexInside = currDataIndexInside); | |
| if (hasOwn(STYLE_VISUAL_TYPE, visualType)) { | |
| const style = data.getItemVisual(dataIndexInside, 'style'); | |
| return style | |
| ? style[STYLE_VISUAL_TYPE[visualType as StyleVisualProps]] as any | |
| : null; | |
| } | |
| // Only support these visuals. Other visual might be inner tricky | |
| // for performance (like `style`), do not expose to users. | |
| if (hasOwn(NON_STYLE_VISUAL_PROPS, visualType)) { | |
| return data.getItemVisual(dataIndexInside, visualType as NonStyleVisualProps) as any; | |
| } | |
| } | |
| /** | |
| * @public | |
| * @return If not support, return undefined. | |
| */ | |
| function barLayout( | |
| opt: Omit<Parameters<typeof getLayoutOnAxis>[0], 'axis'> | |
| ): ReturnType<typeof getLayoutOnAxis> { | |
| if (coordSys.type === 'cartesian2d') { | |
| const baseAxis = coordSys.getBaseAxis() as Axis2D; | |
| return getLayoutOnAxis(defaults({axis: baseAxis}, opt)); | |
| } | |
| } | |
| /** | |
| * @public | |
| */ | |
| function currentSeriesIndices(): ReturnType<GlobalModel['getCurrentSeriesIndices']> { | |
| return ecModel.getCurrentSeriesIndices(); | |
| } | |
| /** | |
| * @public | |
| * @return font string | |
| */ | |
| function font( | |
| opt: Parameters<typeof labelStyleHelper.getFont>[0] | |
| ): ReturnType<typeof labelStyleHelper.getFont> { | |
| return labelStyleHelper.getFont(opt, ecModel); | |
| } | |
| } | |
| function wrapEncodeDef(data: SeriesData<CustomSeriesModel>): WrapEncodeDefRet { | |
| const encodeDef = {} as WrapEncodeDefRet; | |
| each(data.dimensions, function (dimName) { | |
| const dimInfo = data.getDimensionInfo(dimName); | |
| if (!dimInfo.isExtraCoord) { | |
| const coordDim = dimInfo.coordDim; | |
| const dataDims = encodeDef[coordDim] = encodeDef[coordDim] || []; | |
| dataDims[dimInfo.coordDimIndex] = data.getDimensionIndex(dimName); | |
| } | |
| }); | |
| return encodeDef; | |
| } | |
| function createOrUpdateItem( | |
| api: ExtensionAPI, | |
| existsEl: Element, | |
| dataIndex: number, | |
| elOption: CustomRootElementOption, | |
| seriesModel: CustomSeriesModel, | |
| group: ViewRootGroup, | |
| data: SeriesData<CustomSeriesModel> | |
| ): Element { | |
| // [Rule] | |
| // If `renderItem` returns `null`/`undefined`/`false`, remove the previous el if existing. | |
| // (It seems that violate the "merge" principle, but most of users probably intuitively | |
| // regard "return;" as "show nothing element whatever", so make a exception to meet the | |
| // most cases.) | |
| // The rule or "merge" see [STRATEGY_MERGE]. | |
| // If `elOption` is `null`/`undefined`/`false` (when `renderItem` returns nothing). | |
| if (!elOption) { | |
| group.remove(existsEl); | |
| return; | |
| } | |
| const el = doCreateOrUpdateEl(api, existsEl, dataIndex, elOption, seriesModel, group); | |
| el && data.setItemGraphicEl(dataIndex, el); | |
| el && toggleHoverEmphasis( | |
| el, | |
| elOption.focus, | |
| elOption.blurScope, | |
| elOption.emphasisDisabled | |
| ); | |
| return el; | |
| } | |
| function doCreateOrUpdateEl( | |
| api: ExtensionAPI, | |
| existsEl: Element, | |
| dataIndex: number, | |
| elOption: CustomElementOption, | |
| seriesModel: CustomSeriesModel, | |
| group: ViewRootGroup | |
| ): Element { | |
| if (__DEV__) { | |
| assert(elOption, 'should not have an null/undefined element setting'); | |
| } | |
| let toBeReplacedIdx = -1; | |
| const oldEl = existsEl; | |
| if ( | |
| existsEl && ( | |
| doesElNeedRecreate(existsEl, elOption, seriesModel) | |
| // || ( | |
| // // PENDING: even in one-to-one mapping case, if el is marked as morph, | |
| // // do not sure whether the el will be mapped to another el with different | |
| // // hierarchy in Group tree. So always recreate el rather than reuse the el. | |
| // morphHelper && morphHelper.isOneToOneFrom(el) | |
| // ) | |
| ) | |
| ) { | |
| // Should keep at the original index, otherwise "merge by index" will be incorrect. | |
| toBeReplacedIdx = indexOf(group.childrenRef(), existsEl); | |
| existsEl = null; | |
| } | |
| const isInit = !existsEl; | |
| let el = existsEl; | |
| if (!el) { | |
| el = createEl(elOption); | |
| if (oldEl) { | |
| copyElement(oldEl, el); | |
| } | |
| } | |
| else { | |
| // FIMXE:NEXT unified clearState? | |
| // If in some case the performance issue arised, consider | |
| // do not clearState but update cached normal state directly. | |
| el.clearStates(); | |
| } | |
| // Need to set morph: false explictly to disable automatically morphing. | |
| if ((elOption as CustomBaseZRPathOption).morph === false) { | |
| (el as ECElement).disableMorphing = true; | |
| } | |
| else if ((el as ECElement).disableMorphing) { | |
| (el as ECElement).disableMorphing = false; | |
| } | |
| attachedTxInfoTmp.normal.cfg = attachedTxInfoTmp.normal.conOpt = | |
| attachedTxInfoTmp.emphasis.cfg = attachedTxInfoTmp.emphasis.conOpt = | |
| attachedTxInfoTmp.blur.cfg = attachedTxInfoTmp.blur.conOpt = | |
| attachedTxInfoTmp.select.cfg = attachedTxInfoTmp.select.conOpt = null; | |
| attachedTxInfoTmp.isLegacy = false; | |
| doCreateOrUpdateAttachedTx( | |
| el, dataIndex, elOption, seriesModel, isInit, attachedTxInfoTmp | |
| ); | |
| doCreateOrUpdateClipPath( | |
| el, dataIndex, elOption, seriesModel, isInit | |
| ); | |
| updateElNormal( | |
| api, | |
| el, | |
| dataIndex, | |
| elOption, | |
| attachedTxInfoTmp, | |
| seriesModel, | |
| 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); | |
| } | |
| } | |
| updateZ(el, elOption, seriesModel); | |
| if (elOption.type === 'group') { | |
| mergeChildren( | |
| api, el as graphicUtil.Group, dataIndex, elOption as CustomGroupOption, seriesModel | |
| ); | |
| } | |
| if (toBeReplacedIdx >= 0) { | |
| group.replaceAt(el, toBeReplacedIdx); | |
| } | |
| else { | |
| group.add(el); | |
| } | |
| return el; | |
| } | |
| // `el` must not be null/undefined. | |
| function doesElNeedRecreate(el: Element, elOption: CustomElementOption, seriesModel: CustomSeriesModel): boolean { | |
| const elInner = customInnerStore(el); | |
| const elOptionType = elOption.type; | |
| const elOptionShape = (elOption as CustomBaseZRPathOption).shape; | |
| const elOptionStyle = (elOption as CustomDisplayableOption).style; | |
| return ( | |
| // Always create new if universal transition is enabled. | |
| // Because we do transition after render. It needs to know what old element is. Replacement will loose it. | |
| seriesModel.isUniversalTransitionEnabled() | |
| // If `elOptionType` is `null`, follow the merge principle. | |
| || (elOptionType != null | |
| && elOptionType !== elInner.customGraphicType | |
| ) | |
| || (elOptionType === 'path' | |
| && hasOwnPathData(elOptionShape as CustomSVGPathOption['shape']) | |
| && getPathData(elOptionShape as CustomSVGPathOption['shape']) !== elInner.customPathData | |
| ) | |
| || (elOptionType === 'image' | |
| && hasOwn(elOptionStyle, 'image') | |
| && (elOptionStyle as CustomImageOption['style']).image !== elInner.customImagePath | |
| ) | |
| // // FIXME test and remove this restriction? | |
| // || (elOptionType === 'text' | |
| // && hasOwn(elOptionStyle, 'text') | |
| // && (elOptionStyle as TextStyleProps).text !== elInner.customText | |
| // ) | |
| ); | |
| } | |
| function doCreateOrUpdateClipPath( | |
| el: Element, | |
| dataIndex: number, | |
| elOption: CustomElementOption, | |
| seriesModel: CustomSeriesModel, | |
| isInit: boolean | |
| ): void { | |
| // Based on the "merge" principle, if no clipPath provided, | |
| // do nothing. The exists clip will be totally removed only if | |
| // `el.clipPath` is `false`. Otherwise it will be merged/replaced. | |
| const clipPathOpt = elOption.clipPath as CustomPathOption | false; | |
| if (clipPathOpt === false) { | |
| if (el && el.getClipPath()) { | |
| el.removeClipPath(); | |
| } | |
| } | |
| else if (clipPathOpt) { | |
| let clipPath = el.getClipPath(); | |
| if (clipPath && doesElNeedRecreate( | |
| clipPath, | |
| clipPathOpt, | |
| seriesModel | |
| )) { | |
| clipPath = null; | |
| } | |
| if (!clipPath) { | |
| clipPath = createEl(clipPathOpt) as graphicUtil.Path; | |
| if (__DEV__) { | |
| assert( | |
| isPath(clipPath), | |
| 'Only any type of `path` can be used in `clipPath`, rather than ' + clipPath.type + '.' | |
| ); | |
| } | |
| el.setClipPath(clipPath); | |
| } | |
| updateElNormal( | |
| null, clipPath, dataIndex, clipPathOpt, null, seriesModel, isInit | |
| ); | |
| } | |
| // If not define `clipPath` in option, do nothing unnecessary. | |
| } | |
| function doCreateOrUpdateAttachedTx( | |
| el: Element, | |
| dataIndex: number, | |
| elOption: CustomElementOption, | |
| seriesModel: CustomSeriesModel, | |
| isInit: boolean, | |
| attachedTxInfo: AttachedTxInfo | |
| ): void { | |
| // Group does not support textContent temporarily until necessary. | |
| if (el.isGroup) { | |
| return; | |
| } | |
| // Normal must be called before emphasis, for `isLegacy` detection. | |
| processTxInfo(elOption, null, attachedTxInfo); | |
| processTxInfo(elOption, EMPHASIS, attachedTxInfo); | |
| // If `elOption.textConfig` or `elOption.textContent` is null/undefined, it does not make sense. | |
| // So for simplicity, if "elOption hasOwnProperty of them but be null/undefined", we do not | |
| // trade them as set to null to el. | |
| // Especially: | |
| // `elOption.textContent: false` means remove textContent. | |
| // `elOption.textContent.emphasis.style: false` means remove the style from emphasis state. | |
| let txConOptNormal = attachedTxInfo.normal.conOpt as CustomElementOption | false; | |
| const txConOptEmphasis = attachedTxInfo.emphasis.conOpt as CustomElementOptionOnState; | |
| const txConOptBlur = attachedTxInfo.blur.conOpt as CustomElementOptionOnState; | |
| const txConOptSelect = attachedTxInfo.select.conOpt as CustomElementOptionOnState; | |
| if (txConOptNormal != null || txConOptEmphasis != null || txConOptSelect != null || txConOptBlur != null) { | |
| let textContent = el.getTextContent(); | |
| if (txConOptNormal === false) { | |
| textContent && el.removeTextContent(); | |
| } | |
| else { | |
| txConOptNormal = attachedTxInfo.normal.conOpt = txConOptNormal || {type: 'text'}; | |
| if (!textContent) { | |
| textContent = createEl(txConOptNormal) as graphicUtil.Text; | |
| el.setTextContent(textContent); | |
| } | |
| else { | |
| // If in some case the performance issue arised, consider | |
| // do not clearState but update cached normal state directly. | |
| textContent.clearStates(); | |
| } | |
| 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]; | |
| if (stateName !== NORMAL) { | |
| const txConOptOtherState = attachedTxInfo[stateName].conOpt as CustomElementOptionOnState; | |
| updateElOnState( | |
| stateName, | |
| textContent, | |
| txConOptOtherState, | |
| retrieveStyleOptionOnState(txConOptNormal, txConOptOtherState, stateName), | |
| null | |
| ); | |
| } | |
| } | |
| txConStlOptNormal ? textContent.dirty() : textContent.markRedraw(); | |
| } | |
| } | |
| } | |
| function processTxInfo( | |
| elOption: CustomElementOption, | |
| state: DisplayStateNonNormal, | |
| attachedTxInfo: AttachedTxInfo | |
| ): void { | |
| const stateOpt = !state ? elOption : retrieveStateOption(elOption, state); | |
| const styleOpt = !state | |
| ? (elOption as CustomDisplayableOption).style | |
| : retrieveStyleOptionOnState(elOption, stateOpt, EMPHASIS); | |
| const elType = elOption.type; | |
| let txCfg = stateOpt ? stateOpt.textConfig : null; | |
| const txConOptNormal = elOption.textContent; | |
| let txConOpt: CustomElementOption | CustomElementOptionOnState = | |
| !txConOptNormal ? null : !state ? txConOptNormal : retrieveStateOption(txConOptNormal, state); | |
| if (styleOpt && ( | |
| // Because emphasis style has little info to detect legacy, | |
| // if normal is legacy, emphasis is trade as legacy. | |
| attachedTxInfo.isLegacy | |
| || isEC4CompatibleStyle(styleOpt, elType, !!txCfg, !!txConOpt) | |
| )) { | |
| attachedTxInfo.isLegacy = true; | |
| const convertResult = convertFromEC4CompatibleStyle(styleOpt, elType, !state); | |
| // Explicitly specified `textConfig` and `textContent` has higher priority than | |
| // the ones generated by legacy style. Otherwise if users use them and `api.style` | |
| // at the same time, they not both work and hardly to known why. | |
| if (!txCfg && convertResult.textConfig) { | |
| txCfg = convertResult.textConfig; | |
| } | |
| if (!txConOpt && convertResult.textContent) { | |
| txConOpt = convertResult.textContent; | |
| } | |
| } | |
| if (!state && txConOpt) { | |
| const txConOptNormal = txConOpt as CustomElementOption; | |
| // `textContent: {type: 'text'}`, the "type" is easy to be missing. So we tolerate it. | |
| !txConOptNormal.type && (txConOptNormal.type = 'text'); | |
| if (__DEV__) { | |
| // Do not tolerate incorrcet type for forward compat. | |
| assert( | |
| txConOptNormal.type === 'text', | |
| 'textContent.type must be "text"' | |
| ); | |
| } | |
| } | |
| const info = !state ? attachedTxInfo.normal : attachedTxInfo[state]; | |
| info.cfg = txCfg; | |
| info.conOpt = txConOpt; | |
| } | |
| function retrieveStateOption( | |
| elOption: CustomElementOption, state: DisplayStateNonNormal | |
| ): CustomElementOptionOnState { | |
| return !state ? elOption : elOption ? (elOption as CustomDisplayableOption)[state] : null; | |
| } | |
| function retrieveStyleOptionOnState( | |
| stateOptionNormal: CustomElementOption, | |
| stateOption: CustomElementOptionOnState, | |
| state: DisplayStateNonNormal | |
| ): CustomElementOptionOnState['style'] { | |
| let style = stateOption && stateOption.style; | |
| if (style == null && state === EMPHASIS && stateOptionNormal) { | |
| style = (stateOptionNormal as CustomDisplayableOption).styleEmphasis; | |
| } | |
| return style; | |
| } | |
| // Usage: | |
| // (1) By default, `elOption.$mergeChildren` is `'byIndex'`, which indicates | |
| // that the existing children will not be removed, and enables the feature | |
| // that update some of the props of some of the children simply by construct | |
| // the returned children of `renderItem` like: | |
| // `var children = group.children = []; children[3] = {opacity: 0.5};` | |
| // (2) If `elOption.$mergeChildren` is `'byName'`, add/update/remove children | |
| // by child.name. But that might be lower performance. | |
| // (3) If `elOption.$mergeChildren` is `false`, the existing children will be | |
| // replaced totally. | |
| // (4) If `!elOption.children`, following the "merge" principle, nothing will | |
| // happen. | |
| // (5) If `elOption.$mergeChildren` is not `false` neither `'byName'` and the | |
| // `el` is a group, and if any of the new child is null, it means to remove | |
| // the element at the same index, if exists. On the other hand, if the new | |
| // child is and empty object `{}`, it means to keep the element not changed. | |
| // | |
| // For implementation simpleness, do not provide a direct way to remove single | |
| // child (otherwise the total indices of the children array have to be modified). | |
| // User can remove a single child by setting its `ignore` to `true`. | |
| function mergeChildren( | |
| api: ExtensionAPI, | |
| el: graphicUtil.Group, | |
| dataIndex: number, | |
| elOption: CustomGroupOption, | |
| seriesModel: CustomSeriesModel | |
| ): void { | |
| const newChildren = elOption.children; | |
| const newLen = newChildren ? newChildren.length : 0; | |
| const mergeChildren = elOption.$mergeChildren; | |
| // `diffChildrenByName` has been deprecated. | |
| const byName = mergeChildren === 'byName' || elOption.diffChildrenByName; | |
| const notMerge = mergeChildren === false; | |
| // For better performance on roam update, only enter if necessary. | |
| if (!newLen && !byName && !notMerge) { | |
| return; | |
| } | |
| if (byName) { | |
| diffGroupChildren({ | |
| api: api, | |
| oldChildren: el.children() || [], | |
| newChildren: newChildren as CustomElementOption[] || [], | |
| dataIndex: dataIndex, | |
| seriesModel: seriesModel, | |
| group: el | |
| }); | |
| return; | |
| } | |
| notMerge && el.removeAll(); | |
| // Mapping children of a group simply by index, which | |
| // might be better performance. | |
| let index = 0; | |
| for (; index < newLen; index++) { | |
| const newChild = newChildren[index]; | |
| const oldChild = el.childAt(index); | |
| if (newChild) { | |
| if (newChild.ignore == null) { | |
| // The old child is set to be ignored if null (see comments | |
| // below). So we need to set ignore to be false back. | |
| newChild.ignore = false; | |
| } | |
| doCreateOrUpdateEl( | |
| api, | |
| oldChild, | |
| dataIndex, | |
| newChild as CustomElementOption, | |
| seriesModel, | |
| el | |
| ); | |
| } | |
| else { | |
| if (__DEV__) { | |
| assert( | |
| oldChild, | |
| 'renderItem should not return a group containing elements' | |
| + ' as null/undefined/{} if they do not exist before.' | |
| ); | |
| } | |
| // If the new element option is null, it means to remove the old | |
| // element. But we cannot really remove the element from the group | |
| // directly, because the element order may not be stable when this | |
| // element is added back. So we set the element to be ignored. | |
| oldChild.ignore = true; | |
| } | |
| } | |
| for (let i = el.childCount() - 1; i >= index; i--) { | |
| const child = el.childAt(i); | |
| removeChildFromGroup(el, child, seriesModel); | |
| } | |
| } | |
| function removeChildFromGroup( | |
| group: graphicUtil.Group, | |
| child: Element, | |
| seriesModel: SeriesModel | |
| ) { | |
| // Do not support leave elements that are not mentioned in the latest | |
| // `renderItem` return. Otherwise users may not have a clear and simple | |
| // concept that how to control all of the elements. | |
| child && applyLeaveTransition( | |
| child, | |
| customInnerStore(group).option, | |
| seriesModel | |
| ); | |
| } | |
| type DiffGroupContext = { | |
| api: ExtensionAPI; | |
| oldChildren: Element[]; | |
| newChildren: CustomElementOption[]; | |
| dataIndex: number; | |
| seriesModel: CustomSeriesModel; | |
| group: graphicUtil.Group; | |
| }; | |
| function diffGroupChildren(context: DiffGroupContext) { | |
| (new DataDiffer( | |
| context.oldChildren, | |
| context.newChildren, | |
| getKey, | |
| getKey, | |
| context | |
| )) | |
| .add(processAddUpdate) | |
| .update(processAddUpdate) | |
| .remove(processRemove) | |
| .execute(); | |
| } | |
| function getKey(item: Element, idx: number): string { | |
| const name = item && item.name; | |
| return name != null ? name : GROUP_DIFF_PREFIX + idx; | |
| } | |
| function processAddUpdate( | |
| this: DataDiffer<DiffGroupContext>, | |
| newIndex: number, | |
| oldIndex?: number | |
| ): void { | |
| const context = this.context; | |
| const childOption = newIndex != null ? context.newChildren[newIndex] : null; | |
| const child = oldIndex != null ? context.oldChildren[oldIndex] : null; | |
| doCreateOrUpdateEl( | |
| context.api, | |
| child, | |
| context.dataIndex, | |
| childOption, | |
| context.seriesModel, | |
| context.group | |
| ); | |
| } | |
| function processRemove(this: DataDiffer<DiffGroupContext>, oldIndex: number): void { | |
| const context = this.context; | |
| const child = context.oldChildren[oldIndex]; | |
| child && applyLeaveTransition(child, customInnerStore(child).option, context.seriesModel); | |
| } | |
| /** | |
| * @return SVG Path data. | |
| */ | |
| function getPathData(shape: CustomSVGPathOption['shape']): string { | |
| // "d" follows the SVG convention. | |
| return shape && (shape.pathData || shape.d); | |
| } | |
| function hasOwnPathData(shape: CustomSVGPathOption['shape']): boolean { | |
| return shape && (hasOwn(shape, 'pathData') || hasOwn(shape, 'd')); | |
| } | |