/* * 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. */ /** * AUTO-GENERATED FILE. DO NOT MODIFY. */ /* * 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 { SERIES_UNIVERSAL_TRANSITION_PROP } from '../model/Series.js'; import { createHashMap, each, map, filter, isArray, extend } from 'zrender/lib/core/util.js'; import { applyMorphAnimation, getPathList } from './morphTransitionHelper.js'; import Path from 'zrender/lib/graphic/Path.js'; import { initProps } from '../util/graphic.js'; import DataDiffer from '../data/DataDiffer.js'; import { makeInner, normalizeToArray } from '../util/model.js'; import { warn } from '../util/log.js'; import { getAnimationConfig, getOldStyle } from './basicTransition.js'; import Displayable from 'zrender/lib/graphic/Displayable.js'; var DATA_COUNT_THRESHOLD = 1e4; var TRANSITION_NONE = 0; var TRANSITION_P2C = 1; var TRANSITION_C2P = 2; ; var getUniversalTransitionGlobalStore = makeInner(); function getDimension(data, visualDimension) { var dimensions = data.dimensions; for (var i = 0; i < dimensions.length; i++) { var dimInfo = data.getDimensionInfo(dimensions[i]); if (dimInfo && dimInfo.otherDims[visualDimension] === 0) { return dimensions[i]; } } } // get value by dimension. (only get value of itemGroupId or childGroupId, so convert it to string) function getValueByDimension(data, dataIndex, dimension) { var dimInfo = data.getDimensionInfo(dimension); var dimOrdinalMeta = dimInfo && dimInfo.ordinalMeta; if (dimInfo) { var value = data.get(dimInfo.name, dataIndex); if (dimOrdinalMeta) { return dimOrdinalMeta.categories[value] || value + ''; } return value + ''; } } function getGroupId(data, dataIndex, dataGroupId, isChild) { // try to get groupId from encode var visualDimension = isChild ? 'itemChildGroupId' : 'itemGroupId'; var groupIdDim = getDimension(data, visualDimension); if (groupIdDim) { var groupId = getValueByDimension(data, dataIndex, groupIdDim); return groupId; } // try to get groupId from raw data item var rawDataItem = data.getRawDataItem(dataIndex); var 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) { var items = []; each(list, function (seriesInfo) { var data = seriesInfo.data; var dataGroupId = seriesInfo.dataGroupId; if (data.count() > DATA_COUNT_THRESHOLD) { if (process.env.NODE_ENV !== 'production') { warn('Universal transition is disabled on large data > 10k.'); } return; } var indices = data.getIndices(); for (var dataIndex = 0; dataIndex < indices.length; dataIndex++) { items.push({ data: data, groupId: getGroupId(data, dataIndex, dataGroupId, false), childGroupId: getGroupId(data, dataIndex, dataGroupId, true), divide: seriesInfo.divide, dataIndex: dataIndex }); } }); return items; } function fadeInElement(newEl, newSeries, newIndex) { newEl.traverse(function (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) { if (el.parent) { // Bake parent transform to element. // So it can still have proper transform to transition after it's removed. var computedTransform = el.getComputedTransform(); el.setLocalTransform(computedTransform); el.parent.remove(el); } } function stopAnimation(el) { el.stopAnimation(); if (el.isGroup) { el.traverse(function (child) { child.stopAnimation(); }); } } function animateElementStyles(el, dataIndex, seriesModel) { var animationConfig = getAnimationConfig('update', seriesModel, dataIndex); animationConfig && el.traverse(function (child) { if (child instanceof Displayable) { var oldStyle = getOldStyle(child); if (oldStyle) { child.animateFrom({ style: oldStyle }, animationConfig); } } }); } function isAllIdSame(oldDiffItems, newDiffItems) { var len = oldDiffItems.length; if (len !== newDiffItems.length) { return false; } for (var i = 0; i < len; i++) { var oldItem = oldDiffItems[i]; var newItem = newDiffItems[i]; if (oldItem.data.getId(oldItem.dataIndex) !== newItem.data.getId(newItem.dataIndex)) { return false; } } return true; } function transitionBetween(oldList, newList, api) { var oldDiffItems = flattenDataDiffItems(oldList); var newDiffItems = flattenDataDiffItems(newList); function updateMorphingPathProps(from, to, rawFrom, rawTo, animationCfg) { 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); } } var 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'; * } */ var direction = TRANSITION_NONE; // find all groupIds and childGroupIds from oldDiffItems var oldGroupIds = createHashMap(); var oldChildGroupIds = createHashMap(); oldDiffItems.forEach(function (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 (var i = 0; i < newDiffItems.length; i++) { var newGroupId = newDiffItems[i].groupId; if (oldChildGroupIds.get(newGroupId)) { direction = TRANSITION_P2C; break; } var newChildGroupId = newDiffItems[i].childGroupId; if (newChildGroupId && oldGroupIds.get(newChildGroupId)) { direction = TRANSITION_C2P; break; } } function createKeyGetter(isOld, onlyGetId) { return function (diffItem) { var data = diffItem.data; var 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. var useId = isAllIdSame(oldDiffItems, newDiffItems); var isElementStillInChart = {}; 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 (var i = 0; i < newDiffItems.length; i++) { var newItem = newDiffItems[i]; var el = newItem.data.getItemGraphicEl(newItem.dataIndex); if (el) { isElementStillInChart[el.id] = true; } } } function updateOneToOne(newIndex, oldIndex) { var oldItem = oldDiffItems[oldIndex]; var newItem = newDiffItems[newIndex]; var newSeries = newItem.data.hostModel; // TODO Mark this elements is morphed and don't morph them anymore var oldEl = oldItem.data.getItemGraphicEl(oldItem.dataIndex); var 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) { var newItem = newDiffItems[newIndex]; var newData = newItem.data; var newSeries = newData.hostModel; var newEl = newData.getItemGraphicEl(newItem.dataIndex); var oldElsList = filter(map(oldIndices, function (idx) { return oldDiffItems[idx].data.getItemGraphicEl(oldDiffItems[idx].dataIndex); }), function (oldEl) { return 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, function (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) { var oldItem = oldDiffItems[oldIndex]; var oldEl = oldItem.data.getItemGraphicEl(oldItem.dataIndex); // We can't use the elements that already being morphed if (oldEl && isElementStillInChart[oldEl.id]) { return; } var newElsList = filter(map(newIndices, function (idx) { return newDiffItems[idx].data.getItemGraphicEl(newDiffItems[idx].dataIndex); }), function (el) { return el && el !== oldEl; }); var newSeris = newDiffItems[newIndices[0]].data.hostModel; if (newElsList.length) { each(newElsList, function (newEl) { return 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, function (newEl) { return 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, function (rawIdx) { return oldDiffItems[rawIdx].data.getId(oldDiffItems[rawIdx].dataIndex); }, function (rawIdx) { return newDiffItems[rawIdx].data.getId(newDiffItems[rawIdx].dataIndex); }).update(function (newIndex, oldIndex) { // Use the original index updateOneToOne(newIndices[newIndex], oldIndices[oldIndex]); }).execute(); }).execute(); if (hasMorphAnimation) { each(newList, function (_a) { var data = _a.data; var seriesModel = data.hostModel; var view = seriesModel && api.getViewOfSeriesModel(seriesModel); var animationCfg = getAnimationConfig('update', seriesModel, 0); // use 0 index. if (view && seriesModel.isAnimationEnabled() && animationCfg && animationCfg.duration > 0) { view.group.traverse(function (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) { var seriesKey = series.getModel('universalTransition').get('seriesKey'); if (!seriesKey) { // Use series id by default. return series.id; } return seriesKey; } function convertArraySeriesKeyToString(seriesKey) { if (isArray(seriesKey)) { // Order independent. return seriesKey.sort().join(','); } return seriesKey; } function getDivideShapeFromData(data) { if (data.hostModel) { return data.hostModel.getModel('universalTransition').get('divideShape'); } } function findTransitionSeriesBatches(globalStore, params) { var updateBatches = createHashMap(); var oldDataMap = createHashMap(); // Map that only store key in array seriesKey. // Which is used to query the old data when transition from one to multiple series. var oldDataMapForSplit = createHashMap(); each(globalStore.oldSeries, function (series, idx) { var oldDataGroupId = globalStore.oldDataGroupIds[idx]; var oldData = globalStore.oldData[idx]; var transitionKey = getSeriesTransitionKey(series); var transitionKeyStr = convertArraySeriesKeyToString(transitionKey); oldDataMap.set(transitionKeyStr, { dataGroupId: oldDataGroupId, data: oldData }); if (isArray(transitionKey)) { // Same key can't in different array seriesKey. each(transitionKey, function (key) { oldDataMapForSplit.set(key, { key: transitionKeyStr, dataGroupId: oldDataGroupId, data: oldData }); }); } }); function checkTransitionSeriesKeyDuplicated(transitionKeyStr) { if (updateBatches.get(transitionKeyStr)) { warn("Duplicated seriesKey in universalTransition " + transitionKeyStr); } } each(params.updatedSeries, function (series) { if (series.isUniversalTransitionEnabled() && series.isAnimationEnabled()) { var newDataGroupId = series.get('dataGroupId'); var newData = series.getData(); var transitionKey = getSeriesTransitionKey(series); var transitionKeyStr = convertArraySeriesKeyToString(transitionKey); // Only transition between series with same id. var oldData = oldDataMap.get(transitionKeyStr); // string transition key is the best match. if (oldData) { if (process.env.NODE_ENV !== 'production') { 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. // e.g. 'female', 'male' -> ['female', 'male'] if (isArray(transitionKey)) { if (process.env.NODE_ENV !== 'production') { checkTransitionSeriesKeyDuplicated(transitionKeyStr); } var oldSeries_1 = []; each(transitionKey, function (key) { var oldData = oldDataMap.get(key); if (oldData.data) { oldSeries_1.push({ dataGroupId: oldData.dataGroupId, divide: getDivideShapeFromData(oldData.data), data: oldData.data }); } }); if (oldSeries_1.length) { updateBatches.set(transitionKeyStr, { oldSeries: oldSeries_1, newSeries: [{ dataGroupId: newDataGroupId, data: newData, divide: getDivideShapeFromData(newData) }] }); } } else { // Try transition to multiple series. // e.g. ['female', 'male'] -> 'female', 'male' var oldData_1 = oldDataMapForSplit.get(transitionKey); if (oldData_1) { var batch = updateBatches.get(oldData_1.key); if (!batch) { batch = { oldSeries: [{ dataGroupId: oldData_1.dataGroupId, data: oldData_1.data, divide: getDivideShapeFromData(oldData_1.data) }], newSeries: [] }; updateBatches.set(oldData_1.key, batch); } batch.newSeries.push({ dataGroupId: newDataGroupId, data: newData, divide: getDivideShapeFromData(newData) }); } } } } }); return updateBatches; } function querySeries(series, finder) { for (var i = 0; i < series.length; i++) { var found = finder.seriesIndex != null && finder.seriesIndex === series[i].seriesIndex || finder.seriesId != null && finder.seriesId === series[i].id; if (found) { return i; } } } function transitionSeriesFromOpt(transitionOpt, globalStore, params, api) { var from = []; var to = []; each(normalizeToArray(transitionOpt.from), function (finder) { var 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]), groupIdDim: finder.dimension }); } }); each(normalizeToArray(transitionOpt.to), function (finder) { var idx = querySeries(params.updatedSeries, finder); if (idx >= 0) { var data = params.updatedSeries[idx].getData(); to.push({ dataGroupId: globalStore.oldDataGroupIds[idx], data: data, divide: getDivideShapeFromData(data), groupIdDim: finder.dimension }); } }); if (from.length > 0 && to.length > 0) { transitionBetween(from, to, api); } } export function installUniversalTransition(registers) { registers.registerUpdateLifecycle('series:beforeupdate', function (ecMOdel, api, params) { each(normalizeToArray(params.seriesTransition), function (transOpt) { each(normalizeToArray(transOpt.to), function (finder) { var series = params.updatedSeries; for (var 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', function (ecModel, api, params) { // TODO api provide an namespace that can save stuff per instance var globalStore = getUniversalTransitionGlobalStore(api); // 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 // Use give transition config if its' give; var transitionOpt = params.seriesTransition; if (transitionOpt) { each(normalizeToArray(transitionOpt), function (opt) { transitionSeriesFromOpt(opt, globalStore, params, api); }); } else { // Else guess from series based on transition series key. var updateBatches_1 = findTransitionSeriesBatches(globalStore, params); each(updateBatches_1.keys(), function (key) { var batch = updateBatches_1.get(key); transitionBetween(batch.oldSeries, batch.newSeries, api); }); } // Reset each(params.updatedSeries, function (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. var allSeries = ecModel.getSeries(); var savedSeries = globalStore.oldSeries = []; var savedDataGroupIds = globalStore.oldDataGroupIds = []; var savedData = globalStore.oldData = []; for (var i = 0; i < allSeries.length; i++) { var 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')); savedData.push(data); } } }); }