This is an automated email from the ASF dual-hosted git repository. shenyi pushed a commit to branch enhance-morph in repository https://gitbox.apache.org/repos/asf/echarts.git
The following commit(s) were added to refs/heads/enhance-morph by this push: new feb0656 feat(ut): keep the original transition working feb0656 is described below commit feb0656a0e85f83c9995a82d89c844f36b4eb1fb Author: pissang <bm2736...@gmail.com> AuthorDate: Thu Jun 17 16:56:46 2021 +0800 feat(ut): keep the original transition working --- src/animation/universalTransition.ts | 311 ++++++++++++++++++++--------------- src/chart/custom/CustomView.ts | 2 +- src/core/echarts.ts | 49 ++++-- src/core/lifecycle.ts | 21 +++ src/model/Series.ts | 22 +-- test/custom-shape-morphing3.html | 4 - test/universalTransition2.html | 6 +- 7 files changed, 246 insertions(+), 169 deletions(-) diff --git a/src/animation/universalTransition.ts b/src/animation/universalTransition.ts index 600f301..9e6f97c 100644 --- a/src/animation/universalTransition.ts +++ b/src/animation/universalTransition.ts @@ -28,20 +28,40 @@ import { EChartsExtensionInstallRegisters } from '../extension'; import { initProps } from '../util/graphic'; import DataDiffer from '../data/DataDiffer'; import List from '../data/List'; -import { OptionDataItemObject } from '../util/types'; +import { DimensionLoose, OptionDataItemObject } from '../util/types'; +import { UpdateLifecycleParams, UpdateLifecycleTransitionItem } from '../core/lifecycle'; interface DiffItem { data: List + dim: DimensionLoose dataIndex: number } -function flattenDataDiffItems(dataList: List[]) { +interface TransitionSeries { + data: List + dim?: DimensionLoose +} + +function getGroupIdDimension(data: List) { + 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(dataList, data => { + each(list, seriesInfo => { + const data = seriesInfo.data; const indices = data.getIndices(); + const groupDim = getGroupIdDimension(data); for (let dataIndex = 0; dataIndex < indices.length; dataIndex++) { items.push({ data, + dim: seriesInfo.dim || groupDim, dataIndex }); } @@ -50,20 +70,13 @@ function flattenDataDiffItems(dataList: List[]) { return items; } -function transitionBetweenData( - oldDataList: List[], - newDataList: List[] +function transitionBetween( + oldList: TransitionSeries[], + newList: TransitionSeries[] ) { - const oldDiffItems = flattenDataDiffItems(oldDataList); - const newDiffItems = flattenDataDiffItems(newDataList); - // // No data or data are in the same series. - // if (!oldData || !newData || oldData === newData) { - // return; - // } - - // const oldSeriesModel = oldData.hostModel; - // const isTransitionSameSeries = oldSeriesModel === seriesModel; + const oldDiffItems = flattenDataDiffItems(oldList); + const newDiffItems = flattenDataDiffItems(newList); function stopAnimation(el: Element) { el.stopAnimation(); @@ -74,22 +87,6 @@ function transitionBetweenData( } } - // function stopAnimation(pathList: Path[] | Path[][]) { - // if (isArray(pathList[0])) { - // for (let i = 0; i < pathList.length; i++) { - // stopAnimation(pathList[i] as Path[]); - // } - // } - // else { - // // TODO Group itself should also invoke the callback. - // // Force finish the leave animation. - // for (let i = 0; i < pathList.length; i++) { - // (pathList as Path[])[i].stopAnimation(); - // } - // } - // return pathList; - // } - function updateMorphingPathProps( from: Path, to: Path, rawFrom: Path, rawTo: Path, @@ -126,23 +123,17 @@ function transitionBetweenData( } } - function getGroupIdDimension(data: List) { - 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 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); - - // TODO Query from all data. - const oldDataGroupIdDim = getGroupIdDimension(oldDataList[0]); - const newDataGroupIdDim = getGroupIdDimension(newDataList[0]); - - // TODO share it to other modules. or put it in the List - function createGroupIdGetter(isOld: boolean) { + function createKeyGetter(isOld: boolean) { return function (diffItem: DiffItem): string { const data = diffItem.data; const dataIndex = diffItem.dataIndex; @@ -152,25 +143,25 @@ function transitionBetweenData( // If group id not exits. Use id instead. If so, only one to one transition will be applied. const dataGroupId = data.hostModel && (data.hostModel as SeriesModel).get('dataGroupId') as string; - // If one data has groupId encode dimension. Use this same dimension from other data. - // PENDING: If only use groupId dimension of newData. - const groupIdDimension: string = isOld - ? (oldDataGroupIdDim || newDataGroupIdDim) - : (newDataGroupIdDim || oldDataGroupIdDim); + // 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 = groupIdDimension && data.getDimensionInfo(groupIdDimension); + const dimInfo = keyDim && data.getDimensionInfo(keyDim); const dimOrdinalMeta = dimInfo && dimInfo.ordinalMeta; - if (groupIdDimension) { + if (keyDim) { // Get from encode.itemGroupId. - const groupId = data.get(groupIdDimension, dataIndex); + const key = data.get(dimInfo.name, dataIndex); if (dimOrdinalMeta) { - return dimOrdinalMeta.categories[groupId as number] as string || (groupId + ''); + return dimOrdinalMeta.categories[key as number] as string || (key + ''); } - return groupId + ''; + return key + ''; } - // Get from raw item. { groupId: '' } + // Get groupId from raw item. { groupId: '' } const itemVal = data.getRawDataItem(dataIndex) as OptionDataItemObject<unknown>; if (itemVal && itemVal.groupId) { return itemVal.groupId + ''; @@ -194,6 +185,7 @@ function transitionBetweenData( return; } + if (newEl) { // TODO: If keep animating the group in case // some of the elements don't want to be morphed. @@ -223,8 +215,8 @@ function transitionBetweenData( (new DataDiffer( oldDiffItems, newDiffItems, - createGroupIdGetter(true), - createGroupIdGetter(false), + createKeyGetter(true), + createKeyGetter(false), null, 'multiple' )) @@ -330,94 +322,145 @@ function convertArraySeriesKeyToString(seriesKey: string[] | string) { return seriesKey; } -export function installUniversalTransition(registers: EChartsExtensionInstallRegisters) { - registers.registerUpdateLifecycle('series:transition', (ecModel, api, params) => { - // TODO multiple to multiple series. - if (params.oldSeries && params.updatedSeries) { - const updateBatches = createHashMap<{ - oldDataList: List[] - newDataList: List[] - }>(); - - const oldDataMap = createHashMap<List>(); - // 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, - data: List - }>(); - - each(params.oldSeries, (series, idx) => { - const oldData = params.oldData[idx]; - const transitionKey = getSeriesTransitionKey(series); - const transitionKeyStr = convertArraySeriesKeyToString(transitionKey); - oldDataMap.set(transitionKeyStr, oldData); +interface SeriesTransitionBatch { + oldSeries: TransitionSeries[] + newSeries: TransitionSeries[] +} +function findTransitionSeriesBatches(params: UpdateLifecycleParams) { + const updateBatches = createHashMap<SeriesTransitionBatch>(); + + const oldDataMap = createHashMap<List>(); + // 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, + data: List + }>(); + + each(params.oldSeries, (series, idx) => { + const oldData = params.oldData[idx]; + const transitionKey = getSeriesTransitionKey(series); + const transitionKeyStr = convertArraySeriesKeyToString(transitionKey); + oldDataMap.set(transitionKeyStr, oldData); + + if (isArray(transitionKey)) { + // Same key can't in different array seriesKey. + each(transitionKey, key => { + oldDataMapForSplit.set(key, { + data: oldData, + key: transitionKeyStr + }); + }); + } + }); + each(params.updatedSeries, series => { + if (series.get(['universalTransition', 'enabled'])) { + 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) { + // TODO check if data is same? + updateBatches.set(transitionKeyStr, { + oldSeries: [{ + data: oldData + }], + newSeries: [{ + data: newData + }] + }); + } + if (!oldData) { + // Transition from multiple series. if (isArray(transitionKey)) { - // Same key can't in different array seriesKey. + const oldSeries: TransitionSeries[] = []; each(transitionKey, key => { - oldDataMapForSplit.set(key, { - data: oldData, - key: transitionKeyStr - }); + const oldData = oldDataMap.get(key); + if (oldData) { + oldSeries.push({ + data: oldData + }); + } }); - } - }); - each(params.updatedSeries, series => { - if (series.get(['universalTransition', 'enabled'])) { - 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) { - // TODO check if data is same? + if (oldSeries.length) { updateBatches.set(transitionKeyStr, { - oldDataList: [oldData], - newDataList: [newData] + oldSeries, + newSeries: [{ data: newData }] }); } - if (!oldData) { - // Transition from multiple series. - if (isArray(transitionKey)) { - const oldDataList: List[] = []; - each(transitionKey, key => { - const oldData = oldDataMap.get(key); - if (oldData) { - oldDataList.push(oldData); - } - }); - if (oldDataList.length) { - updateBatches.set(transitionKeyStr, { - oldDataList, - newDataList: [newData] - }); - } - } - else { - // Try transition to multiple series. - const oldData = oldDataMapForSplit.get(transitionKey); - if (oldData) { - let batch = updateBatches.get(oldData.key); - if (!batch) { - batch = { - oldDataList: [oldData.data], - newDataList: [] - }; - updateBatches.set(oldData.key, batch); - } - batch.newDataList.push(newData); - } + } + else { + // Try transition to multiple series. + const oldData = oldDataMapForSplit.get(transitionKey); + if (oldData) { + let batch = updateBatches.get(oldData.key); + if (!batch) { + batch = { + oldSeries: [{ data: oldData.data }], + newSeries: [] + }; + updateBatches.set(oldData.key, batch); } + batch.newSeries.push({ + data: newData + }); } } - }); + } + } + }); - each(updateBatches.keys(), key => { - const batch = updateBatches.get(key); - transitionBetweenData(batch.oldDataList, batch.newDataList); - }); + return updateBatches; +} + +function querySeries(series: SeriesModel[], finder: UpdateLifecycleTransitionItem['from']) { + 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, params: UpdateLifecycleParams) { + const fromSeriesIdx = querySeries(params.oldSeries, transitionOpt.from); + const toSeriesIdx = querySeries(params.updatedSeries, transitionOpt.to); + if (fromSeriesIdx >= 0 && toSeriesIdx >= 0) { + transitionBetween([{ + data: params.oldData[fromSeriesIdx], + dim: transitionOpt.from.dimension + }], [{ + data: params.updatedSeries[toSeriesIdx].getData(), + dim: transitionOpt.to.dimension + }]); + } +} + +export function installUniversalTransition(registers: EChartsExtensionInstallRegisters) { + registers.registerUpdateLifecycle('series:transition', (ecModel, api, params) => { + // TODO multiple to multiple series. + if (params.oldSeries && params.updatedSeries) { + // Use give transition config if its' give; + const transitionOpt = params.seriesTransition; + if (transitionOpt) { + if (!isArray(transitionOpt)) { + transitionSeriesFromOpt(transitionOpt, params); + } + else { + each(transitionOpt, opt => transitionSeriesFromOpt(opt, params)); + } + } + else { // Else guess from series based on transition series key. + const updateBatches = findTransitionSeriesBatches(params); + each(updateBatches.keys(), key => { + const batch = updateBatches.get(key); + transitionBetween(batch.oldSeries, batch.newSeries); + }); + } } }); } \ No newline at end of file diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index ed88d72..79004ed 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -216,7 +216,7 @@ export default class CustomChartView extends ChartView { group.removeAll(); } - if (customSeries.get(['universalTransition', 'enabled'])) { + if (customSeries.isUniversalTransitionEnabled()) { // Always create new if universalTransition is enabled. // So the old will not be replaced and can be transition later. // TODO check if UniversalTransition feature is installed. diff --git a/src/core/echarts.ts b/src/core/echarts.ts index acf34da..6f9351f 100644 --- a/src/core/echarts.ts +++ b/src/core/echarts.ts @@ -49,7 +49,7 @@ import OptionManager from '../model/OptionManager'; import backwardCompat from '../preprocessor/backwardCompat'; import dataStack from '../processor/dataStack'; import ComponentModel from '../model/Component'; -import SeriesModel from '../model/Series'; +import SeriesModel, { SERIES_UNIVERSAL_TRANSITION_PROP } from '../model/Series'; import ComponentView, {ComponentViewConstructor} from '../view/Component'; import ChartView, {ChartViewConstructor} from '../view/Chart'; import * as graphic from '../util/graphic'; @@ -127,7 +127,12 @@ import decal from '../visual/decal'; import CanvasPainter from 'zrender/src/canvas/Painter'; import SVGPainter from 'zrender/src/svg/Painter'; import geoSourceManager from '../coord/geo/geoSourceManager'; -import lifecycle, { LifecycleEvents, UpdateLifecycleParams } from './lifecycle'; +import lifecycle, { + LifecycleEvents, + UpdateLifecycleTransitionItem, + UpdateLifecycleParams, + UpdateLifecycleTransitionOpt +} from './lifecycle'; declare let global: any; @@ -208,6 +213,9 @@ type ConnectStatus = | typeof CONNECT_STATUS_UPDATING | typeof CONNECT_STATUS_UPDATED; +export type SetOptionTransitionOpt = UpdateLifecycleTransitionOpt; +export type SetOptionTransitionOptItem = UpdateLifecycleTransitionItem; + export interface SetOptionOpts { notMerge?: boolean; lazyUpdate?: boolean; @@ -218,13 +226,6 @@ export interface SetOptionOpts { transition?: SetOptionTransitionOpt }; -export interface SetOptionTransitionOptItem { - // If `from` not given, it means that do not make series transition mandatorily. - // There might be transition mapping dy default. Sometimes we do not need them, - // which might bring about misleading. - from?: SetOptionTransitionOptFinder; - to: SetOptionTransitionOptFinder; -}; export interface ResizeOpts { width?: number | 'auto', // Can be 'auto' (the same as null/undefined) @@ -233,11 +234,6 @@ export interface ResizeOpts { silent?: boolean // by default false. }; -interface SetOptionTransitionOptFinder extends modelUtil.ModelFinderObject { - dimension: DimensionLoose; -} -type SetOptionTransitionOpt = SetOptionTransitionOptItem | SetOptionTransitionOptItem[]; - interface PostIniter { (chart: EChartsType): void } @@ -594,10 +590,12 @@ class ECharts extends Eventful<ECEventDefinition> { let silent; let replaceMerge; + let transitionOpt: SetOptionTransitionOpt; if (isObject(notMerge)) { lazyUpdate = notMerge.lazyUpdate; silent = notMerge.silent; replaceMerge = notMerge.replaceMerge; + transitionOpt = notMerge.transition; notMerge = notMerge.notMerge; } @@ -620,9 +618,25 @@ class ECharts extends Eventful<ECEventDefinition> { this._model.setOption(option as ECBasicOption, { replaceMerge }, optionPreprocessorFuncs); + if (transitionOpt) { + each(modelUtil.normalizeToArray(transitionOpt), transOpt => { + const finder = transOpt.to; + if (finder) { + const series = this._model.getSeries(); + 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; + } + } + } + }); + } + const updateParams = { oldSeries: oldSeriesModels, - oldData: oldSeriesData + oldData: oldSeriesData, + seriesTransition: transitionOpt } as UpdateLifecycleParams; if (lazyUpdate) { @@ -2065,7 +2079,10 @@ class ECharts extends Eventful<ECEventDefinition> { unfinished = true; } - // seriesModel.uniTransitionMap = null; + // Reset; + if (seriesModel[SERIES_UNIVERSAL_TRANSITION_PROP]) { + seriesModel[SERIES_UNIVERSAL_TRANSITION_PROP] = false; + } chartView.group.silent = !!seriesModel.get('silent'); // Should not call markRedraw on group, because it will disable zrender diff --git a/src/core/lifecycle.ts b/src/core/lifecycle.ts index 78fbda2..7aa91cd 100644 --- a/src/core/lifecycle.ts +++ b/src/core/lifecycle.ts @@ -23,12 +23,33 @@ import GlobalModel from '../model/Global'; import { EChartsType } from './echarts'; import ExtensionAPI from './ExtensionAPI'; import List from '../data/List'; +import { ModelFinderIdQuery, ModelFinderIndexQuery } from '../util/model'; +import { DimensionLoose } from '../util/types'; + +interface TransitionSeriesFinder { + seriesIndex?: ModelFinderIndexQuery, + seriesId?: ModelFinderIdQuery + dimension: DimensionLoose; +} + +export interface UpdateLifecycleTransitionItem { + // If `from` not given, it means that do not make series transition mandatorily. + // There might be transition mapping dy default. Sometimes we do not need them, + // which might bring about misleading. + from?: TransitionSeriesFinder; + to: TransitionSeriesFinder; +}; + +export type UpdateLifecycleTransitionOpt = UpdateLifecycleTransitionItem | UpdateLifecycleTransitionItem[]; export interface UpdateLifecycleParams { oldSeries?: SeriesModel[] oldData?: List[] updatedSeries?: SeriesModel[] + + // Specify series to transition in this setOption. + seriesTransition?: UpdateLifecycleTransitionOpt } interface LifecycleEvents { 'afterinit': [EChartsType], diff --git a/src/model/Series.ts b/src/model/Series.ts index 7ba76a8..0450842 100644 --- a/src/model/Series.ts +++ b/src/model/Series.ts @@ -23,7 +23,7 @@ import * as modelUtil from '../util/model'; import { DataHost, DimensionName, StageHandlerProgressParams, SeriesOption, ZRColor, BoxLayoutOptionMixin, - ScaleDataValue, Dictionary, OptionDataItemObject, SeriesDataType, DimensionLoose + ScaleDataValue, Dictionary, OptionDataItemObject, SeriesDataType } from '../util/types'; import ComponentModel, { ComponentModelConstructor } from './Component'; import {PaletteMixin} from './mixin/palette'; @@ -61,6 +61,8 @@ function getSelectionKey(data: List, dataIndex: number): string { return data.getName(dataIndex) || data.getId(dataIndex); } +export const SERIES_UNIVERSAL_TRANSITION_PROP = '__universalTransitionEnabled'; + interface SeriesModel { /** * Convinient for override in extended class. @@ -140,16 +142,6 @@ class SeriesModel<Opt extends SeriesOption = SeriesOption> extends ComponentMode // Injected outside pipelineContext: PipelineContext; - // Id for mapping universalTransition - uniTransitionId: string; - // Dimension map for universal animation between series. - // only avalible in `render()` caused by `setOption`. - uniTransitionMap: { - // Both from and to can be null/undefined, which means no transform mapping. - from: DimensionLoose; - to: DimensionLoose; - }; - // --------------------------------------- // Props to tell visual/style.ts about how to do visual encoding. // --------------------------------------- @@ -174,6 +166,10 @@ class SeriesModel<Opt extends SeriesOption = SeriesOption> extends ComponentMode // Symbol provide to legend. legendIcon: string; + // It will be set temporary when cross series transition setting is from setOption. + // TODO if deprecate further? + [SERIES_UNIVERSAL_TRANSITION_PROP]: boolean; + // --------------------------------------- // Props about data selection // --------------------------------------- @@ -540,6 +536,10 @@ class SeriesModel<Opt extends SeriesOption = SeriesOption> extends ComponentMode return selectedMap[nameOrId] || false; } + isUniversalTransitionEnabled() { + return this[SERIES_UNIVERSAL_TRANSITION_PROP] || this.get(['universalTransition', 'enabled']); + } + private _innerSelect(data: List, innerDataIndices: number[]) { const selectedMode = this.option.selectedMode; const len = innerDataIndices.length; diff --git a/test/custom-shape-morphing3.html b/test/custom-shape-morphing3.html index c184334..a5765f0 100644 --- a/test/custom-shape-morphing3.html +++ b/test/custom-shape-morphing3.html @@ -165,7 +165,6 @@ under the License. children: [{ type: 'rect', transition: ['shape', 'style'], - morph: true, shape: { x: basePos[0], y: basePos[1], @@ -218,7 +217,6 @@ under the License. children: [{ type: 'rect', transition: ['shape', 'style'], - morph: true, shape: { x: basePos[0], y: basePos[1], @@ -270,7 +268,6 @@ under the License. children: [{ type: 'circle', transition: ['shape', 'style'], - morph: true, shape: { cx: pos[0], cy: pos[1], @@ -323,7 +320,6 @@ under the License. children: [{ type: 'rect', transition: ['shape', 'style'], - morph: true, shape: { x: basePos[0], y: valPos[1] - height / 2, diff --git a/test/universalTransition2.html b/test/universalTransition2.html index c2aa3ae..079a28f 100644 --- a/test/universalTransition2.html +++ b/test/universalTransition2.html @@ -166,8 +166,8 @@ under the License. const RAW_DATA_DIMENSIONS = ['Income', 'Life Expectancy', 'Population', 'Country', 'Year']; const SUM_INCOME_DIMENSIONS = ['Income', 'Country']; - var COUNTRY_A = 'Germany'; - var COUNTRY_B = 'France'; + const COUNTRY_A = 'Germany'; + const COUNTRY_B = 'France'; const datasetOption = [ { @@ -312,7 +312,7 @@ under the License. option: createOption('Year', 'Population', 'bar') }, { - option: createOption('Year', 'Population', 'bar', '', 'main') + option: createOption('Year', 'Population', 'bar', '', 'stack') }, { option: createOption('Income', 'Country', 'pie', 'Country') --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@echarts.apache.org For additional commands, e-mail: commits-h...@echarts.apache.org