| /* | 
 | * Licensed to the Apache Software Foundation (ASF) under one | 
 | * or more contributor license agreements.  See the NOTICE file | 
 | * distributed with this work for additional information | 
 | * regarding copyright ownership.  The ASF licenses this file | 
 | * to you under the Apache License, Version 2.0 (the | 
 | * "License"); you may not use this file except in compliance | 
 | * with the License.  You may obtain a copy of the License at | 
 | * | 
 | *   http://www.apache.org/licenses/LICENSE-2.0 | 
 | * | 
 | * Unless required by applicable law or agreed to in writing, | 
 | * software distributed under the License is distributed on an | 
 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | 
 | * KIND, either express or implied.  See the License for the | 
 | * specific language governing permissions and limitations | 
 | * under the License. | 
 | */ | 
 |  | 
 | // Universal transitions that can animate between any shapes(series) and any properties in any amounts. | 
 |  | 
 | import SeriesModel, { SERIES_UNIVERSAL_TRANSITION_PROP } from '../model/Series'; | 
 | import {createHashMap, each, map, filter, isArray, extend} from 'zrender/src/core/util'; | 
 | import Element, { ElementAnimateConfig } from 'zrender/src/Element'; | 
 | import { applyMorphAnimation, getPathList } from './morphTransitionHelper'; | 
 | import Path from 'zrender/src/graphic/Path'; | 
 | 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 { | 
 |     UpdateLifecycleParams, | 
 |     UpdateLifecycleTransitionItem, | 
 |     UpdateLifecycleTransitionSeriesFinder | 
 | } from '../core/lifecycle'; | 
 | import { makeInner, normalizeToArray } from '../util/model'; | 
 | import { warn } from '../util/log'; | 
 | import ExtensionAPI from '../core/ExtensionAPI'; | 
 | import { getAnimationConfig, getOldStyle } from './basicTransition'; | 
 | import Model from '../model/Model'; | 
 | import Displayable from 'zrender/src/graphic/Displayable'; | 
 |  | 
 | const DATA_COUNT_THRESHOLD = 1e4; | 
 |  | 
 | interface GlobalStore { oldSeries: SeriesModel[], oldDataGroupIds: string[], oldData: SeriesData[] }; | 
 | const getUniversalTransitionGlobalStore = makeInner<GlobalStore, ExtensionAPI>(); | 
 |  | 
 | interface DiffItem { | 
 |     dataGroupId: string | 
 |     data: SeriesData | 
 |     dim: DimensionLoose | 
 |     divide: UniversalTransitionOption['divideShape'] | 
 |     dataIndex: number | 
 | } | 
 | interface TransitionSeries { | 
 |     dataGroupId: string | 
 |     data: SeriesData | 
 |     divide: UniversalTransitionOption['divideShape'] | 
 |     dim?: DimensionLoose | 
 | } | 
 |  | 
 | function getGroupIdDimension(data: SeriesData) { | 
 |     const dimensions = data.dimensions; | 
 |     for (let i = 0; i < dimensions.length; i++) { | 
 |         const dimInfo = data.getDimensionInfo(dimensions[i]); | 
 |         if (dimInfo && dimInfo.otherDims.itemGroupId === 0) { | 
 |             return dimensions[i]; | 
 |         } | 
 |     } | 
 | } | 
 |  | 
 | function flattenDataDiffItems(list: TransitionSeries[]) { | 
 |     const items: DiffItem[] = []; | 
 |  | 
 |     each(list, seriesInfo => { | 
 |         const data = seriesInfo.data; | 
 |         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, | 
 |                 divide: seriesInfo.divide, | 
 |                 dataIndex | 
 |             }); | 
 |         } | 
 |     }); | 
 |  | 
 |     return items; | 
 | } | 
 |  | 
 |  | 
 | function fadeInElement(newEl: Element, newSeries: SeriesModel, newIndex: number) { | 
 |     newEl.traverse(el => { | 
 |         if (el instanceof Path) { | 
 |             // TODO use fade in animation for target element. | 
 |             initProps(el, { | 
 |                 style: { | 
 |                     opacity: 0 | 
 |                 } | 
 |             }, newSeries, { | 
 |                 dataIndex: newIndex, | 
 |                 isFrom: true | 
 |             }); | 
 |         } | 
 |     }); | 
 | } | 
 | function removeEl(el: Element) { | 
 |     if (el.parent) { | 
 |         // Bake parent transform to element. | 
 |         // So it can still have proper transform to transition after it's removed. | 
 |         const computedTransform = el.getComputedTransform(); | 
 |         el.setLocalTransform(computedTransform); | 
 |         el.parent.remove(el); | 
 |     } | 
 | } | 
 | function stopAnimation(el: Element) { | 
 |     el.stopAnimation(); | 
 |     if (el.isGroup) { | 
 |         el.traverse(child => { | 
 |             child.stopAnimation(); | 
 |         }); | 
 |     } | 
 | } | 
 | function animateElementStyles(el: Element, dataIndex: number, seriesModel: SeriesModel) { | 
 |     const animationConfig = getAnimationConfig('update', seriesModel, dataIndex); | 
 |     animationConfig && el.traverse(child => { | 
 |         if (child instanceof Displayable) { | 
 |             const oldStyle = getOldStyle(child); | 
 |             if (oldStyle) { | 
 |                 child.animateFrom({ | 
 |                     style: oldStyle | 
 |                 }, animationConfig); | 
 |             } | 
 |         } | 
 |     }); | 
 | } | 
 |  | 
 |  | 
 | function isAllIdSame(oldDiffItems: DiffItem[], newDiffItems: DiffItem[]) { | 
 |     const len = oldDiffItems.length; | 
 |     if (len !== newDiffItems.length) { | 
 |         return false; | 
 |     } | 
 |     for (let i = 0; i < len; i++) { | 
 |         const oldItem = oldDiffItems[i]; | 
 |         const newItem = newDiffItems[i]; | 
 |         if (oldItem.data.getId(oldItem.dataIndex) !== newItem.data.getId(newItem.dataIndex)) { | 
 |             return false; | 
 |         } | 
 |     } | 
 |     return true; | 
 | } | 
 |  | 
 | 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); | 
 |         } | 
 |     } | 
 |  | 
 |  | 
 |     function findKeyDim(items: DiffItem[]) { | 
 |         for (let i = 0; i < items.length; i++) { | 
 |             if (items[i].dim) { | 
 |                 return items[i].dim; | 
 |             } | 
 |         } | 
 |     } | 
 |     const oldKeyDim = findKeyDim(oldDiffItems); | 
 |     const newKeyDim = findKeyDim(newDiffItems); | 
 |  | 
 |     let hasMorphAnimation = false; | 
 |  | 
 |     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); | 
 |             } | 
 |  | 
 |             // 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 + ''; | 
 |             } | 
 |  | 
 |             // Get groupId from raw item. { groupId: '' } | 
 |             const itemVal = data.getRawDataItem(dataIndex) as OptionDataItemObject<unknown>; | 
 |             if (itemVal && itemVal.groupId) { | 
 |                 return itemVal.groupId + ''; | 
 |             } | 
 |             return (dataGroupId || data.getId(dataIndex)); | 
 |         }; | 
 |     } | 
 |  | 
 |     // 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); | 
 |                     } | 
 |                 }); | 
 |             } | 
 |         }); | 
 |     } | 
 | } | 
 |  | 
 | function getSeriesTransitionKey(series: SeriesModel) { | 
 |     const seriesKey = (series.getModel('universalTransition') as Model<UniversalTransitionOption>) | 
 |         .get('seriesKey'); | 
 |     if (!seriesKey) { | 
 |         // Use series id by default. | 
 |         return series.id; | 
 |     } | 
 |     return seriesKey; | 
 | } | 
 |  | 
 | function convertArraySeriesKeyToString(seriesKey: string[] | string) { | 
 |     if (isArray(seriesKey)) { | 
 |         // Order independent. | 
 |         return seriesKey.sort().join(','); | 
 |     } | 
 |     return seriesKey; | 
 | } | 
 |  | 
 | interface SeriesTransitionBatch { | 
 |     oldSeries: TransitionSeries[] | 
 |     newSeries: TransitionSeries[] | 
 | } | 
 |  | 
 | function getDivideShapeFromData(data: SeriesData) { | 
 |     if (data.hostModel) { | 
 |         return ((data.hostModel as SeriesModel) | 
 |             .getModel('universalTransition') as Model<UniversalTransitionOption>) | 
 |             .get('divideShape'); | 
 |     } | 
 | } | 
 |  | 
 | function findTransitionSeriesBatches( | 
 |     globalStore: GlobalStore, | 
 |     params: UpdateLifecycleParams | 
 | ) { | 
 |     const updateBatches = createHashMap<SeriesTransitionBatch>(); | 
 |  | 
 |     const oldDataMap = createHashMap<{ | 
 |         dataGroupId: string, | 
 |         data: SeriesData | 
 |     }>(); | 
 |     // Map that only store key in array seriesKey. | 
 |     // Which is used to query the old data when transition from one to multiple series. | 
 |     const oldDataMapForSplit = createHashMap<{ | 
 |         key: string, | 
 |         dataGroupId: string, | 
 |         data: SeriesData | 
 |     }>(); | 
 |  | 
 |     each(globalStore.oldSeries, (series, idx) => { | 
 |         const oldDataGroupId = globalStore.oldDataGroupIds[idx] as string; | 
 |         const oldData = globalStore.oldData[idx]; | 
 |         const transitionKey = getSeriesTransitionKey(series); | 
 |         const transitionKeyStr = convertArraySeriesKeyToString(transitionKey); | 
 |         oldDataMap.set(transitionKeyStr, { | 
 |             dataGroupId: oldDataGroupId, | 
 |             data: oldData | 
 |         }); | 
 |  | 
 |         if (isArray(transitionKey)) { | 
 |             // Same key can't in different array seriesKey. | 
 |             each(transitionKey, key => { | 
 |                 oldDataMapForSplit.set(key, { | 
 |                     key: transitionKeyStr, | 
 |                     dataGroupId: oldDataGroupId, | 
 |                     data: oldData | 
 |                 }); | 
 |             }); | 
 |         } | 
 |     }); | 
 |  | 
 |     function checkTransitionSeriesKeyDuplicated(transitionKeyStr: string) { | 
 |         if (updateBatches.get(transitionKeyStr)) { | 
 |             warn(`Duplicated seriesKey in universalTransition ${transitionKeyStr}`); | 
 |         } | 
 |     } | 
 |     each(params.updatedSeries, series => { | 
 |         if (series.isUniversalTransitionEnabled() && series.isAnimationEnabled()) { | 
 |             const newDataGroupId = series.get('dataGroupId') as string; | 
 |             const newData = series.getData(); | 
 |             const transitionKey = getSeriesTransitionKey(series); | 
 |             const transitionKeyStr = convertArraySeriesKeyToString(transitionKey); | 
 |             // Only transition between series with same id. | 
 |             const oldData = oldDataMap.get(transitionKeyStr); | 
 |             // string transition key is the best match. | 
 |             if (oldData) { | 
 |                 if (__DEV__) { | 
 |                     checkTransitionSeriesKeyDuplicated(transitionKeyStr); | 
 |                 } | 
 |                 // TODO check if data is same? | 
 |                 updateBatches.set(transitionKeyStr, { | 
 |                     oldSeries: [{ | 
 |                         dataGroupId: oldData.dataGroupId, | 
 |                         divide: getDivideShapeFromData(oldData.data), | 
 |                         data: oldData.data | 
 |                     }], | 
 |                     newSeries: [{ | 
 |                         dataGroupId: newDataGroupId, | 
 |                         divide: getDivideShapeFromData(newData), | 
 |                         data: newData | 
 |                     }] | 
 |                 }); | 
 |             } | 
 |             else { | 
 |                 // Transition from multiple series. | 
 |                 if (isArray(transitionKey)) { | 
 |                     if (__DEV__) { | 
 |                         checkTransitionSeriesKeyDuplicated(transitionKeyStr); | 
 |                     } | 
 |                     const oldSeries: TransitionSeries[] = []; | 
 |                     each(transitionKey, key => { | 
 |                         const oldData = oldDataMap.get(key); | 
 |                         if (oldData.data) { | 
 |                             oldSeries.push({ | 
 |                                 dataGroupId: oldData.dataGroupId, | 
 |                                 divide: getDivideShapeFromData(oldData.data), | 
 |                                 data: oldData.data | 
 |                             }); | 
 |                         } | 
 |                     }); | 
 |                     if (oldSeries.length) { | 
 |                         updateBatches.set(transitionKeyStr, { | 
 |                             oldSeries, | 
 |                             newSeries: [{ | 
 |                                 dataGroupId: newDataGroupId, | 
 |                                 data: newData, | 
 |                                 divide: getDivideShapeFromData(newData) | 
 |                             }] | 
 |                         }); | 
 |                     } | 
 |                 } | 
 |                 else { | 
 |                     // Try transition to multiple series. | 
 |                     const oldData = oldDataMapForSplit.get(transitionKey); | 
 |                     if (oldData) { | 
 |                         let batch = updateBatches.get(oldData.key); | 
 |                         if (!batch) { | 
 |                             batch = { | 
 |                                 oldSeries: [{ | 
 |                                     dataGroupId: oldData.dataGroupId, | 
 |                                     data: oldData.data, | 
 |                                     divide: getDivideShapeFromData(oldData.data) | 
 |                                 }], | 
 |                                 newSeries: [] | 
 |                             }; | 
 |                             updateBatches.set(oldData.key, batch); | 
 |                         } | 
 |                         batch.newSeries.push({ | 
 |                             dataGroupId: newDataGroupId, | 
 |                             data: newData, | 
 |                             divide: getDivideShapeFromData(newData) | 
 |                         }); | 
 |                     } | 
 |                 } | 
 |             } | 
 |         } | 
 |     }); | 
 |  | 
 |     return updateBatches; | 
 | } | 
 |  | 
 | function querySeries(series: SeriesModel[], finder: UpdateLifecycleTransitionSeriesFinder) { | 
 |     for (let i = 0; i < series.length; i++) { | 
 |         const found = finder.seriesIndex != null && finder.seriesIndex === series[i].seriesIndex | 
 |             || finder.seriesId != null && finder.seriesId === series[i].id; | 
 |         if (found) { | 
 |             return i; | 
 |         } | 
 |     } | 
 | } | 
 |  | 
 | function transitionSeriesFromOpt( | 
 |     transitionOpt: UpdateLifecycleTransitionItem, | 
 |     globalStore: GlobalStore, | 
 |     params: UpdateLifecycleParams, | 
 |     api: ExtensionAPI | 
 | ) { | 
 |     const from: TransitionSeries[] = []; | 
 |     const to: TransitionSeries[] = []; | 
 |     each(normalizeToArray(transitionOpt.from), finder => { | 
 |         const idx = querySeries(globalStore.oldSeries, finder); | 
 |         if (idx >= 0) { | 
 |             from.push({ | 
 |                 dataGroupId: globalStore.oldDataGroupIds[idx], | 
 |                 data: globalStore.oldData[idx], | 
 |                 // TODO can specify divideShape in transition. | 
 |                 divide: getDivideShapeFromData(globalStore.oldData[idx]), | 
 |                 dim: finder.dimension | 
 |             }); | 
 |         } | 
 |     }); | 
 |     each(normalizeToArray(transitionOpt.to), finder => { | 
 |         const idx = querySeries(params.updatedSeries, finder); | 
 |         if (idx >= 0) { | 
 |             const data = params.updatedSeries[idx].getData(); | 
 |             to.push({ | 
 |                 dataGroupId: globalStore.oldDataGroupIds[idx], | 
 |                 data, | 
 |                 divide: getDivideShapeFromData(data), | 
 |                 dim: finder.dimension | 
 |             }); | 
 |         } | 
 |     }); | 
 |     if (from.length > 0 && to.length > 0) { | 
 |         transitionBetween(from, to, api); | 
 |     } | 
 | } | 
 |  | 
 | export function installUniversalTransition(registers: EChartsExtensionInstallRegisters) { | 
 |  | 
 |     registers.registerUpdateLifecycle('series:beforeupdate', (ecMOdel, api, params) => { | 
 |         each(normalizeToArray(params.seriesTransition), transOpt => { | 
 |             each(normalizeToArray(transOpt.to), (finder) => { | 
 |                 const series = params.updatedSeries; | 
 |                 for (let i = 0; i < series.length; i++) { | 
 |                     if (finder.seriesIndex != null && finder.seriesIndex === series[i].seriesIndex | 
 |                         || finder.seriesId != null && finder.seriesId === series[i].id) { | 
 |                         series[i][SERIES_UNIVERSAL_TRANSITION_PROP] = true; | 
 |                     } | 
 |                 } | 
 |             }); | 
 |         }); | 
 |     }); | 
 |     registers.registerUpdateLifecycle('series:transition', (ecModel, api, params) => { | 
 |         // TODO api provide an namespace that can save stuff per instance | 
 |         const globalStore = getUniversalTransitionGlobalStore(api); | 
 |  | 
 |         // TODO multiple to multiple series. | 
 |         if (globalStore.oldSeries && params.updatedSeries && params.optionChanged) { | 
 |             // Use give transition config if its' give; | 
 |             const transitionOpt = params.seriesTransition; | 
 |             if (transitionOpt) { | 
 |                 each(normalizeToArray(transitionOpt), opt => { | 
 |                     transitionSeriesFromOpt(opt, globalStore, params, api); | 
 |                 }); | 
 |             } | 
 |             else {  // Else guess from series based on transition series key. | 
 |                 const updateBatches = findTransitionSeriesBatches(globalStore, params); | 
 |                 each(updateBatches.keys(), key => { | 
 |                     const batch = updateBatches.get(key); | 
 |                     transitionBetween(batch.oldSeries, batch.newSeries, api); | 
 |                 }); | 
 |             } | 
 |  | 
 |             // Reset | 
 |             each(params.updatedSeries, series => { | 
 |                 // Reset; | 
 |                 if (series[SERIES_UNIVERSAL_TRANSITION_PROP]) { | 
 |                     series[SERIES_UNIVERSAL_TRANSITION_PROP] = false; | 
 |                 } | 
 |             }); | 
 |         } | 
 |  | 
 |         // Save all series of current update. Not only the updated one. | 
 |         const allSeries = ecModel.getSeries(); | 
 |         const savedSeries: SeriesModel[] = globalStore.oldSeries = []; | 
 |         const savedDataGroupIds: string[] = globalStore.oldDataGroupIds = []; | 
 |         const savedData: SeriesData[] = globalStore.oldData = []; | 
 |         for (let i = 0; i < allSeries.length; i++) { | 
 |             const data = allSeries[i].getData(); | 
 |             // Only save the data that can have transition. | 
 |             // Avoid large data costing too much extra memory | 
 |             if (data.count() < DATA_COUNT_THRESHOLD) { | 
 |                 savedSeries.push(allSeries[i]); | 
 |                 savedDataGroupIds.push(allSeries[i].get('dataGroupId') as string); | 
 |                 savedData.push(data); | 
 |             } | 
 |         } | 
 |     }); | 
 | } |