-
Notifications
You must be signed in to change notification settings - Fork 19.8k
feat(animation): support multi-level drill-down for universal transition #17611
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
6f500be
0f85029
4d4634f
a4cf7f9
788daec
5063c51
01ba523
5e475a2
6e555b7
9fcbc7c
ce89ee7
b123f35
6168f98
e5b9be3
d689521
938e415
8e80c01
55ba825
ffbc6af
5321c2d
dc9a3f6
6f7a395
edd2d3c
9d52862
3d5b32c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
|
@@ -49,45 +56,104 @@ const getUniversalTransitionGlobalStore = makeInner<GlobalStore, ExtensionAPI>() | |
| interface DiffItem { | ||
| dataGroupId: string | ||
| data: SeriesData | ||
| dim: DimensionLoose | ||
| groupId: string | ||
| childGroupId: string | ||
| divide: UniversalTransitionOption['divideShape'] | ||
| dataIndex: number | ||
| } | ||
| interface TransitionSeries { | ||
| dataGroupId: string | ||
| data: SeriesData | ||
| divide: UniversalTransitionOption['divideShape'] | ||
| dim?: DimensionLoose | ||
| groupIdDim?: DimensionLoose | ||
| } | ||
|
|
||
| enum TransitionDirection { | ||
| None = 'none', | ||
| P2C = 'parent -> child', | ||
| C2P = 'child -> parent' | ||
| } | ||
|
|
||
| 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) { | ||
tyn1998 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // try to get groupId from encode | ||
| const groupIdDim = getDimension(data, 'itemGroupId'); | ||
| if (groupIdDim) { | ||
| const groupId = getValueByDimension(data, dataIndex, groupIdDim); | ||
| if (groupId) { | ||
| return groupId; | ||
tyn1998 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| // try to get groupId from raw data item | ||
| const itemVal = data.getRawDataItem(dataIndex) as OptionDataItemObject<unknown>; | ||
| if (itemVal && itemVal.groupId) { | ||
| return itemVal.groupId + ''; | ||
| } | ||
| // try to use series.dataGroupId as groupId | ||
| // if failing to get groupId by all 3 ways above, fallback to data.getId(dataIndex) | ||
| return (dataGroupId || data.getId(dataIndex)); | ||
| } | ||
|
|
||
| function getChildGroupId(data: SeriesData, dataIndex: number) { | ||
| // try to get childGroupId from encode | ||
| const childGroupIdDim = getDimension(data, 'childGroupId'); | ||
| if (childGroupIdDim) { | ||
| const childGroupId = getValueByDimension(data, dataIndex, childGroupIdDim); | ||
| if (childGroupId) { | ||
| return childGroupId; | ||
| } | ||
| } | ||
| // try to get groupId from raw data item | ||
| const itemVal = data.getRawDataItem(dataIndex) as OptionDataItemObject<unknown>; | ||
| if (itemVal && itemVal.childGroupId) { | ||
| return itemVal.childGroupId + ''; | ||
| } | ||
| // if no childGroupId specified, return undefined | ||
| return undefined; | ||
tyn1998 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // 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.'); | ||
| } | ||
| 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), // either of groupId or childGroupId will be used as diffItem's key, | ||
| childGroupId: getChildGroupId(data, dataIndex), // depending on the transition direction (see below) | ||
| divide: seriesInfo.divide, | ||
| dataIndex | ||
| }); | ||
|
|
@@ -185,18 +251,66 @@ 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 too. This direction is "none". | ||
| * | ||
| * So basiclly we need to know whether using groupId or childGroupId as key when we get key from | ||
| * the keyGetter function. 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 = TransitionDirection.None; | ||
|
|
||
| const oldGroupIds = oldDiffItems.filter((item) => item.groupId !== undefined).map((item) => item.groupId); | ||
| const oldChildGroupIds = oldDiffItems | ||
| .filter((item) => item.childGroupId !== undefined) | ||
tyn1998 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| .map((item) => item.childGroupId); | ||
| for (let i = 0; i < newDiffItems.length; i++) { | ||
| const newGroupId = newDiffItems[i].groupId; | ||
| if (oldChildGroupIds.includes(newGroupId)) { | ||
tyn1998 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| direction = TransitionDirection.P2C; | ||
| break; | ||
| } | ||
| const newChildGroupId = newDiffItems[i].childGroupId; | ||
| if (newChildGroupId && oldGroupIds.includes(newChildGroupId)) { | ||
| direction = TransitionDirection.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 +320,22 @@ 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); | ||
|
Comment on lines
-215
to
-219
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This fault-tolerance code is simply dropped. Because the code for getting key from dimension(encode) is brought forward, where accessing both oldKeyDim and newKeyDim is not convinient. In my opinion, the code should better be dropped. Users should get universalTransition working only if they satisfy all needs to trigger a universalTransition as the doc says. If they do not provide a perfect option but our code helps handling what they miss to make universalTransition succeed, they might be confused once they find "a bad option also triggers universalTransition", since they have no idea that echarts developers write fault-tolerance code to help with their bad options. |
||
|
|
||
| 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 + ''); | ||
| if (isOld) { | ||
| if (direction === TransitionDirection.C2P || direction === TransitionDirection.None) { | ||
| return diffItem.groupId; | ||
| } | ||
| if (direction === TransitionDirection.P2C) { | ||
| return diffItem.childGroupId; | ||
| } | ||
| return key + ''; | ||
| } | ||
|
|
||
| // Get groupId from raw item. { groupId: '' } | ||
| const itemVal = data.getRawDataItem(dataIndex) as OptionDataItemObject<unknown>; | ||
| if (itemVal && itemVal.groupId) { | ||
| return itemVal.groupId + ''; | ||
| else { | ||
| if (direction === TransitionDirection.P2C || direction === TransitionDirection.None) { | ||
| return diffItem.groupId; | ||
| } | ||
| if (direction === TransitionDirection.C2P) { | ||
| return diffItem.childGroupId; | ||
| } | ||
| } | ||
| return (dataGroupId || data.getId(dataIndex)); | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -541,6 +641,7 @@ function findTransitionSeriesBatches( | |
| } | ||
| else { | ||
| // Transition from multiple series. | ||
| // e.g. 'female', 'male' -> ['female', 'male'] | ||
| if (isArray(transitionKey)) { | ||
| if (__DEV__) { | ||
| checkTransitionSeriesKeyDuplicated(transitionKeyStr); | ||
|
|
@@ -569,6 +670,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 +725,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 +737,7 @@ function transitionSeriesFromOpt( | |
| dataGroupId: globalStore.oldDataGroupIds[idx], | ||
| data, | ||
| divide: getDivideShapeFromData(data), | ||
| dim: finder.dimension | ||
| groupIdDim: finder.dimension | ||
| }); | ||
| } | ||
| }); | ||
|
|
@@ -665,6 +767,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 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi, @pissang. As you mentioned earlier that |
||
| // Use give transition config if its' give; | ||
| const transitionOpt = params.seriesTransition; | ||
| if (transitionOpt) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.