/* * 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 = { 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, ecModel: GlobalModel, api: ExtensionAPI ) { const renderItem = customSeries.get('renderItem'); const coordSys = customSeries.coordinateSystem; let prepareResult = {} as ReturnType; 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; let currItemStyleModels: Partial>> = {}; let currLabelModels: Partial>> = {}; const seriesItemStyleModels = {} as Record>; const seriesLabelModels = {} as Record>; for (let i = 0; i < STATES.length; i++) { const stateName = STATES[i]; seriesItemStyleModels[stateName] = (customSeries as Model) .getModel(PATH_ITEM_STYLE[stateName]); seriesLabelModels[stateName] = (customSeries as Model) .getModel(PATH_LABEL[stateName]); } function getItemModel(dataIndexInside: number): Model { 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( 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[0], 'axis'> ): ReturnType { if (coordSys.type === 'cartesian2d') { const baseAxis = coordSys.getBaseAxis() as Axis2D; return getLayoutOnAxis(defaults({axis: baseAxis}, opt)); } } /** * @public */ function currentSeriesIndices(): ReturnType { return ecModel.getCurrentSeriesIndices(); } /** * @public * @return font string */ function font( opt: Parameters[0] ): ReturnType { return labelStyleHelper.getFont(opt, ecModel); } } function wrapEncodeDef(data: SeriesData): 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 ): 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, 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, 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')); }