diff --git a/src/animation/universalTransition.ts b/src/animation/universalTransition.ts index c82d3d78f8..c1747bbe7b 100644 --- a/src/animation/universalTransition.ts +++ b/src/animation/universalTransition.ts @@ -28,7 +28,14 @@ import { EChartsExtensionInstallRegisters } from '../extension'; import { initProps } from '../util/graphic'; import DataDiffer from '../data/DataDiffer'; import SeriesData from '../data/SeriesData'; -import { Dictionary, DimensionLoose, OptionDataItemObject, UniversalTransitionOption } from '../util/types'; +import { + Dictionary, + DimensionLoose, + DimensionName, + DataVisualDimensions, + OptionDataItemObject, + UniversalTransitionOption +} from '../util/types'; import { UpdateLifecycleParams, UpdateLifecycleTransitionItem, @@ -42,14 +49,17 @@ import Model from '../model/Model'; import Displayable from 'zrender/src/graphic/Displayable'; const DATA_COUNT_THRESHOLD = 1e4; +const TRANSITION_NONE = 0; +const TRANSITION_P2C = 1; +const TRANSITION_C2P = 2; interface GlobalStore { oldSeries: SeriesModel[], oldDataGroupIds: string[], oldData: SeriesData[] }; const getUniversalTransitionGlobalStore = makeInner(); interface DiffItem { - dataGroupId: string data: SeriesData - dim: DimensionLoose + groupId: string + childGroupId: string divide: UniversalTransitionOption['divideShape'] dataIndex: number } @@ -57,24 +67,61 @@ interface TransitionSeries { dataGroupId: string data: SeriesData divide: UniversalTransitionOption['divideShape'] - dim?: DimensionLoose + groupIdDim?: DimensionLoose } -function getGroupIdDimension(data: SeriesData) { +function getDimension(data: SeriesData, visualDimension: string) { const dimensions = data.dimensions; for (let i = 0; i < dimensions.length; i++) { const dimInfo = data.getDimensionInfo(dimensions[i]); - if (dimInfo && dimInfo.otherDims.itemGroupId === 0) { + if (dimInfo && dimInfo.otherDims[visualDimension as keyof DataVisualDimensions] === 0) { return dimensions[i]; } } } +// get value by dimension. (only get value of itemGroupId or childGroupId, so convert it to string) +function getValueByDimension(data: SeriesData, dataIndex: number, dimension: DimensionName) { + const dimInfo = data.getDimensionInfo(dimension); + const dimOrdinalMeta = dimInfo && dimInfo.ordinalMeta; + if (dimInfo) { + const value = data.get(dimInfo.name, dataIndex); + if (dimOrdinalMeta) { + return (dimOrdinalMeta.categories[value as number] as string) || value + ''; + } + return value + ''; + } +} + +function getGroupId(data: SeriesData, dataIndex: number, dataGroupId: string, isChild: boolean) { + // try to get groupId from encode + const visualDimension = isChild ? 'itemChildGroupId' : 'itemGroupId'; + const groupIdDim = getDimension(data, visualDimension); + if (groupIdDim) { + const groupId = getValueByDimension(data, dataIndex, groupIdDim); + return groupId; + } + // try to get groupId from raw data item + const rawDataItem = data.getRawDataItem(dataIndex) as OptionDataItemObject; + const property = isChild ? 'childGroupId' : 'groupId'; + if (rawDataItem && rawDataItem[property]) { + return rawDataItem[property] + ''; + } + // fallback + if (isChild) { + return; + } + // try to use series.dataGroupId as groupId, otherwise use dataItem's id as groupId + return (dataGroupId || data.getId(dataIndex)); +} + +// flatten all data items from different serieses into one arrary function flattenDataDiffItems(list: TransitionSeries[]) { const items: DiffItem[] = []; each(list, seriesInfo => { const data = seriesInfo.data; + const dataGroupId = seriesInfo.dataGroupId; if (data.count() > DATA_COUNT_THRESHOLD) { if (__DEV__) { warn('Universal transition is disabled on large data > 10k.'); @@ -82,12 +129,11 @@ function flattenDataDiffItems(list: TransitionSeries[]) { return; } const indices = data.getIndices(); - const groupDim = getGroupIdDimension(data); for (let dataIndex = 0; dataIndex < indices.length; dataIndex++) { items.push({ - dataGroupId: seriesInfo.dataGroupId, data, - dim: seriesInfo.dim || groupDim, + groupId: getGroupId(data, dataIndex, dataGroupId, false), // either of groupId or childGroupId will be used as diffItem's key, + childGroupId: getGroupId(data, dataIndex, dataGroupId, true), // depending on the transition direction (see below) divide: seriesInfo.divide, dataIndex }); @@ -185,18 +231,71 @@ function transitionBetween( } } + let hasMorphAnimation = false; - function findKeyDim(items: DiffItem[]) { - for (let i = 0; i < items.length; i++) { - if (items[i].dim) { - return items[i].dim; - } + /** + * With groupId and childGroupId, we can build parent-child relationships between dataItems. + * However, we should mind the parent-child "direction" between old and new options. + * + * For example, suppose we have two dataItems from two series.data: + * + * dataA: [ dataB: [ + * { { + * value: 5, value: 3, + * groupId: 'creatures', groupId: 'animals', + * childGroupId: 'animals' childGroupId: 'dogs' + * }, }, + * ... ... + * ] ] + * + * where dataA is belong to optionA and dataB is belong to optionB. + * + * When we `setOption(optionB)` from optionA, we choose childGroupId of dataItemA and groupId of + * dataItemB as keys so the two keys are matched (both are 'animals'), then universalTransition + * will work. This derection is "parent -> child". + * + * If we `setOption(optionA)` from optionB, we also choose groupId of dataItemB and childGroupId + * of dataItemA as keys and universalTransition will work. This derection is "child -> parent". + * + * If there is no childGroupId specified, which means no multiLevelDrillDown/Up is needed and no + * parent-child relationship exists. This direction is "none". + * + * So we need to know whether to use groupId or childGroupId as the key when we call the keyGetter + * functions. Thus, we need to decide the direction first. + * + * The rule is: + * + * if (all childGroupIds in oldDiffItems and all groupIds in newDiffItems have common value) { + * direction = 'parent -> child'; + * } else if (all groupIds in oldDiffItems and all childGroupIds in newDiffItems have common value) { + * direction = 'child -> parent'; + * } else { + * direction = 'none'; + * } + */ + let direction = TRANSITION_NONE; + + // find all groupIds and childGroupIds from oldDiffItems + const oldGroupIds = createHashMap(); + const oldChildGroupIds = createHashMap(); + oldDiffItems.forEach((item) => { + item.groupId && oldGroupIds.set(item.groupId, true); + item.childGroupId && oldChildGroupIds.set(item.childGroupId, true); + + }); + // traverse newDiffItems and decide the direction according to the rule + for (let i = 0; i < newDiffItems.length; i++) { + const newGroupId = newDiffItems[i].groupId; + if (oldChildGroupIds.get(newGroupId)) { + direction = TRANSITION_P2C; + break; + } + const newChildGroupId = newDiffItems[i].childGroupId; + if (newChildGroupId && oldGroupIds.get(newChildGroupId)) { + direction = TRANSITION_C2P; + break; } } - const oldKeyDim = findKeyDim(oldDiffItems); - const newKeyDim = findKeyDim(newDiffItems); - - let hasMorphAnimation = false; function createKeyGetter(isOld: boolean, onlyGetId: boolean) { return function (diffItem: DiffItem): string { @@ -206,36 +305,12 @@ function transitionBetween( if (onlyGetId) { return data.getId(dataIndex); } - - // Use group id as transition key by default. - // So we can achieve multiple to multiple animation like drilldown / up naturally. - // If group id not exits. Use id instead. If so, only one to one transition will be applied. - const dataGroupId = diffItem.dataGroupId; - - // If specified key dimension(itemGroupId by default). Use this same dimension from other data. - // PENDING: If only use key dimension of newData. - const keyDim = isOld - ? (oldKeyDim || newKeyDim) - : (newKeyDim || oldKeyDim); - - const dimInfo = keyDim && data.getDimensionInfo(keyDim); - const dimOrdinalMeta = dimInfo && dimInfo.ordinalMeta; - - if (dimInfo) { - // Get from encode.itemGroupId. - const key = data.get(dimInfo.name, dataIndex); - if (dimOrdinalMeta) { - return dimOrdinalMeta.categories[key as number] as string || (key + ''); - } - return key + ''; + if (isOld) { + return direction === TRANSITION_P2C ? diffItem.childGroupId : diffItem.groupId; } - - // Get groupId from raw item. { groupId: '' } - const itemVal = data.getRawDataItem(dataIndex) as OptionDataItemObject; - if (itemVal && itemVal.groupId) { - return itemVal.groupId + ''; + else { + return direction === TRANSITION_C2P ? diffItem.childGroupId : diffItem.groupId; } - return (dataGroupId || data.getId(dataIndex)); }; } @@ -541,6 +616,7 @@ function findTransitionSeriesBatches( } else { // Transition from multiple series. + // e.g. 'female', 'male' -> ['female', 'male'] if (isArray(transitionKey)) { if (__DEV__) { checkTransitionSeriesKeyDuplicated(transitionKeyStr); @@ -569,6 +645,7 @@ function findTransitionSeriesBatches( } else { // Try transition to multiple series. + // e.g. ['female', 'male'] -> 'female', 'male' const oldData = oldDataMapForSplit.get(transitionKey); if (oldData) { let batch = updateBatches.get(oldData.key); @@ -623,7 +700,7 @@ function transitionSeriesFromOpt( data: globalStore.oldData[idx], // TODO can specify divideShape in transition. divide: getDivideShapeFromData(globalStore.oldData[idx]), - dim: finder.dimension + groupIdDim: finder.dimension }); } }); @@ -635,7 +712,7 @@ function transitionSeriesFromOpt( dataGroupId: globalStore.oldDataGroupIds[idx], data, divide: getDivideShapeFromData(data), - dim: finder.dimension + groupIdDim: finder.dimension }); } }); @@ -665,6 +742,7 @@ export function installUniversalTransition(registers: EChartsExtensionInstallReg // TODO multiple to multiple series. if (globalStore.oldSeries && params.updatedSeries && params.optionChanged) { + // TODO transitionOpt was used in an old implementation and can be removed now // Use give transition config if its' give; const transitionOpt = params.seriesTransition; if (transitionOpt) { diff --git a/src/util/types.ts b/src/util/types.ts index 36e970da6d..1a038df762 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -430,7 +430,7 @@ export type DimensionLoose = DimensionName | DimensionIndexLoose; export type DimensionType = DataStoreDimensionType; export const VISUAL_DIMENSIONS = createHashMap([ - 'tooltip', 'label', 'itemName', 'itemId', 'itemGroupId', 'seriesName' + 'tooltip', 'label', 'itemName', 'itemId', 'itemGroupId', 'itemChildGroupId', 'seriesName' ]); // The key is VISUAL_DIMENSIONS export interface DataVisualDimensions { @@ -442,6 +442,7 @@ export interface DataVisualDimensions { itemName?: DimensionIndex; itemId?: DimensionIndex; itemGroupId?: DimensionIndex; + itemChildGroupId?: DimensionIndex; seriesName?: DimensionIndex; } @@ -616,6 +617,7 @@ export type OptionDataItemObject = { id?: OptionId; name?: OptionName; groupId?: OptionId; + childGroupId?: OptionId; value?: T[] | T; selected?: boolean; }; @@ -665,6 +667,7 @@ export interface OptionEncodeVisualDimensions { // Which is useful in prepresenting the transition key of drilldown/up animation. // Or hover linking. itemGroupId?: OptionEncodeValue; + childGroupdId?: OptionEncodeValue; } export interface OptionEncode extends OptionEncodeVisualDimensions { [coordDim: string]: OptionEncodeValue | undefined diff --git a/test/universalTransition-multiLevelDrillDown.html b/test/universalTransition-multiLevelDrillDown.html new file mode 100644 index 0000000000..7d40d1aafd --- /dev/null +++ b/test/universalTransition-multiLevelDrillDown.html @@ -0,0 +1,476 @@ + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + +