function transitionBetween()

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);
                    }
                });
            }
        });
    }
}