-
Notifications
You must be signed in to change notification settings - Fork 19.7k
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
feat(animation): support multi-level drill-down for universal transition #17611
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
6f500be
test(universalTransition): a possible test file
tyn1998 0f85029
wip: successfully get childGroupId (a possible new interface)
tyn1998 4d4634f
wip: can decide "parent2child" or "child2parent"
tyn1998 a4cf7f9
wip: new createKeyGetters that suppot multi-drill (but only get key f…
tyn1998 788daec
wip: should consider getting direction from more ways & using groupId…
tyn1998 5063c51
refactor: rename "KeyDim" to "GroupIdDim"
tyn1998 01ba523
learning: add some comments
tyn1998 5e475a2
fix: incorrect dataGroupId for old data items in universalTransition
tyn1998 6e555b7
feat: multiple level drill down (alpha)
tyn1998 9fcbc7c
refactor: more readable universalTransition-multiLevelDrillDown.html
tyn1998 ce89ee7
docs: add comments for code
tyn1998 b123f35
docs: improve comments in test html
tyn1998 6168f98
docs: update universalTransition-multiLevelDrilldown.html
tyn1998 e5b9be3
Merge branch 'master' into feat/multi-drill
tyn1998 d689521
revert changes introduced by a merge in mistake
tyn1998 938e415
rename `childGroupId` to `itemChildGroupId` to make it consistent wit…
tyn1998 8e80c01
no need to return undefined, just return
tyn1998 55ba825
use `!= null` to test if equal to `undefined` or `null`
tyn1998 ffbc6af
not use enum, just use const variables
tyn1998 5321c2d
merge `getGroupId` and `getChildGroupId` into one function
tyn1998 dc9a3f6
use createHashMap() to improve performance
tyn1998 6f7a395
add another test case
tyn1998 edd2d3c
slow down animation update in test cases
tyn1998 9d52862
fix unexpected appearance of xAxis.name after switching options
tyn1998 3d5b32c
simplify code
tyn1998 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
|
@@ -42,52 +49,91 @@ 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<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 | ||
} | ||
|
||
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<unknown>; | ||
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.'); | ||
} | ||
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<unknown>; | ||
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 | ||
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) { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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.