in src/animation/universalTransition.ts [209:512]
function transitionBetween(
oldList: TransitionSeries[],
newList: TransitionSeries[],
api: ExtensionAPI
) {
const oldDiffItems = flattenDataDiffItems(oldList);
const newDiffItems = flattenDataDiffItems(newList);
function updateMorphingPathProps(
from: Path, to: Path,
rawFrom: Path, rawTo: Path,
animationCfg: ElementAnimateConfig
) {
if (rawFrom || from) {
to.animateFrom({
style: (rawFrom && rawFrom !== from)
// dividingMethod like clone may override the style(opacity)
// So extend it to raw style.
? extend(extend({}, rawFrom.style), from.style)
: from.style
}, animationCfg);
}
}
let hasMorphAnimation = false;
/**
* 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;
}
}
function createKeyGetter(isOld: boolean, onlyGetId: boolean) {
return function (diffItem: DiffItem): string {
const data = diffItem.data;
const dataIndex = diffItem.dataIndex;
// TODO if specified dim
if (onlyGetId) {
return data.getId(dataIndex);
}
if (isOld) {
return direction === TRANSITION_P2C ? diffItem.childGroupId : diffItem.groupId;
}
else {
return direction === TRANSITION_C2P ? diffItem.childGroupId : diffItem.groupId;
}
};
}
// Use id if it's very likely to be an one to one animation
// It's more robust than groupId
// TODO Check if key dimension is specified.
const useId = isAllIdSame(oldDiffItems, newDiffItems);
const isElementStillInChart: Dictionary<boolean> = {};
if (!useId) {
// We may have different diff strategy with basicTransition if we use other dimension as key.
// If so, we can't simply check if oldEl is same with newEl. We need a map to check if oldEl is still being used in the new chart.
// We can't use the elements that already being morphed. Let it keep it's original basic transition.
for (let i = 0; i < newDiffItems.length; i++) {
const newItem = newDiffItems[i];
const el = newItem.data.getItemGraphicEl(newItem.dataIndex);
if (el) {
isElementStillInChart[el.id] = true;
}
}
}
function updateOneToOne(newIndex: number, oldIndex: number) {
const oldItem = oldDiffItems[oldIndex];
const newItem = newDiffItems[newIndex];
const newSeries = newItem.data.hostModel as SeriesModel;
// TODO Mark this elements is morphed and don't morph them anymore
const oldEl = oldItem.data.getItemGraphicEl(oldItem.dataIndex);
const newEl = newItem.data.getItemGraphicEl(newItem.dataIndex);
// Can't handle same elements.
if (oldEl === newEl) {
newEl && animateElementStyles(newEl, newItem.dataIndex, newSeries);
return;
}
if (
// We can't use the elements that already being morphed
(oldEl && isElementStillInChart[oldEl.id])
) {
return;
}
if (newEl) {
// TODO: If keep animating the group in case
// some of the elements don't want to be morphed.
// TODO Label?
stopAnimation(newEl);
if (oldEl) {
stopAnimation(oldEl);
// If old element is doing leaving animation. stop it and remove it immediately.
removeEl(oldEl);
hasMorphAnimation = true;
applyMorphAnimation(
getPathList(oldEl),
getPathList(newEl),
newItem.divide,
newSeries,
newIndex,
updateMorphingPathProps
);
}
else {
fadeInElement(newEl, newSeries, newIndex);
}
}
// else keep oldEl leaving animation.
}
(new DataDiffer(
oldDiffItems,
newDiffItems,
createKeyGetter(true, useId),
createKeyGetter(false, useId),
null,
'multiple'
))
.update(updateOneToOne)
.updateManyToOne(function (newIndex, oldIndices) {
const newItem = newDiffItems[newIndex];
const newData = newItem.data;
const newSeries = newData.hostModel as SeriesModel;
const newEl = newData.getItemGraphicEl(newItem.dataIndex);
const oldElsList = filter(
map(oldIndices, idx =>
oldDiffItems[idx].data.getItemGraphicEl(oldDiffItems[idx].dataIndex)
),
oldEl => oldEl && oldEl !== newEl && !isElementStillInChart[oldEl.id]
);
if (newEl) {
stopAnimation(newEl);
if (oldElsList.length) {
// If old element is doing leaving animation. stop it and remove it immediately.
each(oldElsList, oldEl => {
stopAnimation(oldEl);
removeEl(oldEl);
});
hasMorphAnimation = true;
applyMorphAnimation(
getPathList(oldElsList),
getPathList(newEl),
newItem.divide,
newSeries,
newIndex,
updateMorphingPathProps
);
}
else {
fadeInElement(newEl, newSeries, newItem.dataIndex);
}
}
// else keep oldEl leaving animation.
})
.updateOneToMany(function (newIndices, oldIndex) {
const oldItem = oldDiffItems[oldIndex];
const oldEl = oldItem.data.getItemGraphicEl(oldItem.dataIndex);
// We can't use the elements that already being morphed
if (oldEl && isElementStillInChart[oldEl.id]) {
return;
}
const newElsList = filter(
map(newIndices, idx =>
newDiffItems[idx].data.getItemGraphicEl(newDiffItems[idx].dataIndex)
),
el => el && el !== oldEl
);
const newSeris = newDiffItems[newIndices[0]].data.hostModel as SeriesModel;
if (newElsList.length) {
each(newElsList, newEl => stopAnimation(newEl));
if (oldEl) {
stopAnimation(oldEl);
// If old element is doing leaving animation. stop it and remove it immediately.
removeEl(oldEl);
hasMorphAnimation = true;
applyMorphAnimation(
getPathList(oldEl),
getPathList(newElsList),
oldItem.divide, // Use divide on old.
newSeris,
newIndices[0],
updateMorphingPathProps
);
}
else {
each(newElsList, newEl => fadeInElement(newEl, newSeris, newIndices[0]));
}
}
// else keep oldEl leaving animation.
})
.updateManyToMany(function (newIndices, oldIndices) {
// If two data are same and both have groupId.
// Normally they should be diff by id.
new DataDiffer(
oldIndices,
newIndices,
(rawIdx: number) => oldDiffItems[rawIdx].data.getId(oldDiffItems[rawIdx].dataIndex),
(rawIdx: number) => newDiffItems[rawIdx].data.getId(newDiffItems[rawIdx].dataIndex)
).update((newIndex, oldIndex) => {
// Use the original index
updateOneToOne(newIndices[newIndex], oldIndices[oldIndex]);
}).execute();
})
.execute();
if (hasMorphAnimation) {
each(newList, ({ data }) => {
const seriesModel = data.hostModel as SeriesModel;
const view = seriesModel && api.getViewOfSeriesModel(seriesModel as SeriesModel);
const animationCfg = getAnimationConfig('update', seriesModel, 0); // use 0 index.
if (view && seriesModel.isAnimationEnabled() && animationCfg && animationCfg.duration > 0) {
view.group.traverse(el => {
if (el instanceof Path && !el.animators.length) {
// We can't accept there still exists element that has no animation
// if universalTransition is enabled
el.animateFrom({
style: {
opacity: 0
}
}, animationCfg);
}
});
}
});
}
}