You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by su...@apache.org on 2020/09/27 17:46:53 UTC
[incubator-echarts] 02/05: feature: (1) Support custom series
morphing with split/merge effect. (2) Add API: setOption(option,
{ transition: { ... } }) to indicate the transition relationship. (3) Fix
underlying schedule framework buggy disopose when setOption at the second
time and change data.
This is an automated email from the ASF dual-hosted git repository.
sushuang pushed a commit to branch custom-morph2
in repository https://gitbox.apache.org/repos/asf/incubator-echarts.git
commit 82331873c60d100b842a9d70b95c61db963527ae
Author: 100pah <su...@gmail.com>
AuthorDate: Wed Sep 23 17:12:59 2020 +0800
feature:
(1) Support custom series morphing with split/merge effect.
(2) Add API: setOption(option, { transition: { ... } }) to indicate the transition relationship.
(3) Fix underlying schedule framework buggy disopose when setOption at the second time and change data.
---
src/chart/custom.ts | 214 +++++++++++++++------
src/coord/scaleRawExtentInfo.ts | 10 +-
src/data/DataDiffer.ts | 238 ++++++++++++++++++------
src/echarts.ts | 96 +++++++++-
src/model/Global.ts | 8 +-
src/model/Series.ts | 11 +-
src/util/model.ts | 26 ++-
src/util/types.ts | 34 +++-
test/custom-shape-morphing2.html | 388 +++++++++++++++++++++++++++++++++++++++
9 files changed, 892 insertions(+), 133 deletions(-)
diff --git a/src/chart/custom.ts b/src/chart/custom.ts
index 0c645f8..ca87fb4 100644
--- a/src/chart/custom.ts
+++ b/src/chart/custom.ts
@@ -19,7 +19,7 @@
import {
hasOwn, assert, isString, retrieve2, retrieve3, defaults, each,
- keys, isArrayLike, bind, isFunction, eqNaN
+ keys, isArrayLike, bind, isFunction, eqNaN, isArray
} from 'zrender/src/core/util';
import * as graphicUtil from '../util/graphic';
import { setDefaultStateProxy, enableHoverEmphasis } from '../util/states';
@@ -81,8 +81,8 @@ import {
import Transformable from 'zrender/src/core/Transformable';
import { ItemStyleProps } from '../model/mixin/itemStyle';
import { cloneValue } from 'zrender/src/animation/Animator';
-import { warn, error } from '../util/log';
-import { morphPath } from 'zrender/src/tool/morphPath';
+import { warn, error, throwError } from '../util/log';
+import { morphPath, splitShapeForMorphingFrom, isPathMorphing } from 'zrender/src/tool/morphPath';
const inner = makeInner<{
@@ -460,22 +460,58 @@ class CustomSeriesView extends ChartView {
// is complicated, where merge mode is probably necessary for optimization.
// For example, reuse graphic elements and only update the transform when
// roam or data zoom according to `actionType`.
- data.diff(oldData)
- .add(function (newIdx) {
- createOrUpdateItem(
- null, newIdx, renderItem(newIdx, payload), customSeries, group, data
- );
- })
- .update(function (newIdx, oldIdx) {
+
+ const transOpt = customSeries.__transientTransitionOpt;
+
+ (new DataDiffer(
+ oldData ? oldData.getIndices() : [],
+ data.getIndices(),
+ createGetKey(oldData, transOpt && transOpt.from),
+ createGetKey(data, transOpt && transOpt.to),
+ null,
+ transOpt ? 'multiple' : 'single'
+ ))
+ .add(function (newIdx) {
+ createOrUpdateItem(
+ null, newIdx, renderItem(newIdx, payload), customSeries, group, data, null
+ );
+ })
+ .update(function (newIdx, oldIdx) {
+ createOrUpdateItem(
+ oldData.getItemGraphicEl(oldIdx),
+ newIdx, renderItem(newIdx, payload), customSeries, group, data, null
+ );
+ })
+ .remove(function (oldIdx) {
+ doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group);
+ })
+ .updateManyToOne(function (newIdx, oldIndices) {
+ const oldElsToMerge: graphicUtil.Path[] = [];
+ for (let i = 0; i < oldIndices.length; i++) {
+ const oldEl = oldData.getItemGraphicEl(oldIndices[i]);
+ if (elCanMorph(oldEl)) {
+ oldElsToMerge.push(oldEl);
+ }
+ removeElementDirectly(oldEl, group);
+ }
+ createOrUpdateItem(
+ null, newIdx, renderItem(newIdx, payload), customSeries,
+ group, data, oldElsToMerge
+ );
+ })
+ .updateOneToMany(function (newIndices, oldIdx) {
+ const newLen = newIndices.length;
+ const oldEl = oldData.getItemGraphicEl(oldIdx);
+ const oldElSplitted = elCanMorph(oldEl) ? splitShapeForMorphingFrom(oldEl, newLen) : [];
+ removeElementDirectly(oldEl, group);
+ for (let i = 0; i < newLen; i++) {
createOrUpdateItem(
- oldData.getItemGraphicEl(oldIdx),
- newIdx, renderItem(newIdx, payload), customSeries, group, data
+ null, newIndices[i], renderItem(newIndices[i], payload), customSeries,
+ group, data, oldElSplitted[i]
);
- })
- .remove(function (oldIdx) {
- doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group);
- })
- .execute();
+ }
+ })
+ .execute();
// Do clipping
const clipPath = customSeries.get('clip', true)
@@ -516,7 +552,9 @@ class CustomSeriesView extends ChartView {
}
}
for (let idx = params.start; idx < params.end; idx++) {
- const el = createOrUpdateItem(null, idx, renderItem(idx, payload), customSeries, this.group, data);
+ const el = createOrUpdateItem(
+ null, idx, renderItem(idx, payload), customSeries, this.group, data, null
+ );
el.traverse(setIncrementalAndHoverLayer);
}
}
@@ -544,6 +582,40 @@ class CustomSeriesView extends ChartView {
ChartView.registerClass(CustomSeriesView);
+function createGetKey(data: List, dimension: DimensionLoose) {
+ if (!data) {
+ return;
+ }
+
+ const diffBy = data.getDimension(dimension);
+
+ if (diffBy == null) {
+ return function (rawIdx: number, dataIndex: number) {
+ return data.getId(dataIndex);
+ };
+ }
+
+ const dimInfo = data.getDimensionInfo(diffBy);
+ if (!dimInfo) {
+ let errMsg = '';
+ if (__DEV__) {
+ errMsg = `${dimension} is not a valid dimension.`;
+ }
+ throwError(errMsg);
+ }
+ const ordinalMeta = dimInfo.ordinalMeta;
+ return function (rawIdx: number, dataIndex: number) {
+ let key = data.get(diffBy, dataIndex);
+ if (ordinalMeta) {
+ key = ordinalMeta.categories[key as number];
+ }
+ return (key == null || eqNaN(key))
+ ? rawIdx + ''
+ : '_ec_' + key;
+ };
+}
+
+
function createEl(elOption: CustomElementOption): Element {
const graphicType = elOption.type;
let el;
@@ -582,11 +654,13 @@ function createEl(elOption: CustomElementOption): Element {
}
else {
const Clz = graphicUtil.getShapeClass(graphicType);
-
- if (__DEV__) {
- assert(Clz, 'graphic type "' + graphicType + '" can not be found.');
+ if (!Clz) {
+ let errMsg = '';
+ if (__DEV__) {
+ errMsg = 'graphic type "' + graphicType + '" can not be found.';
+ }
+ throwError(errMsg);
}
-
el = new Clz();
}
@@ -654,7 +728,7 @@ function createEl(elOption: CustomElementOption): Element {
*/
function updateElNormal(
el: Element,
- morphingFromEl: Element,
+ morphingFromEl: graphicUtil.Path | graphicUtil.Path[],
dataIndex: number,
elOption: CustomElementOption,
styleOpt: CustomElementOption['style'],
@@ -757,7 +831,7 @@ function updateElNormal(
styleOpt ? el.dirty() : el.markRedraw();
if (morphingFromEl) {
- applyShapeMorphingAnimation(morphingFromEl, el, seriesModel, dataIndex);
+ applyShapeMorphingAnimation(morphingFromEl, el as graphicUtil.Path, seriesModel, dataIndex);
}
}
@@ -766,7 +840,7 @@ function updateElNormal(
function prepareShapeOrExtraUpdate(
mainAttr: 'shape' | 'extra',
el: Element,
- morphingFromEl: Element,
+ morphingFromEl: graphicUtil.Path | graphicUtil.Path[],
elOption: CustomElementOption,
allProps: LooseElementProps,
transFromProps: LooseElementProps,
@@ -834,14 +908,14 @@ function prepareShapeOrExtraUpdate(
// See [STRATEGY_TRANSITION].
function prepareTransformUpdate(
el: Element,
- morphingFromEl: Element,
+ morphingFromEl: graphicUtil.Path | graphicUtil.Path[],
elOption: CustomElementOption,
allProps: ElementProps,
transFromProps: ElementProps,
isInit: boolean
): void {
const enterFrom = elOption.enterFrom;
- const fromEl = morphingFromEl || el;
+ const fromEl = (morphingFromEl instanceof graphicUtil.Path) && morphingFromEl || el;
if (isInit && enterFrom) {
const enterFromKeys = keys(enterFrom);
for (let i = 0; i < enterFromKeys.length; i++) {
@@ -904,7 +978,7 @@ function prepareTransformUpdate(
// See [STRATEGY_TRANSITION].
function prepareStyleUpdate(
el: Element,
- morphingFromEl: Element,
+ morphingFromEl: graphicUtil.Path | graphicUtil.Path[],
styleOpt: CustomElementOption['style'],
transFromProps: LooseElementProps,
isInit: boolean
@@ -913,7 +987,7 @@ function prepareStyleUpdate(
return;
}
- const fromEl = morphingFromEl || el;
+ const fromEl = (morphingFromEl instanceof graphicUtil.Path) && morphingFromEl || el;
const fromElStyle = (fromEl as LooseElementProps).style as LooseElementProps['style'];
let transFromStyleProps: LooseElementProps['style'];
@@ -1546,7 +1620,8 @@ function createOrUpdateItem(
elOption: CustomElementOption,
seriesModel: CustomSeriesModel,
group: ViewRootGroup,
- data: List<CustomSeriesModel>
+ data: List<CustomSeriesModel>,
+ morphingFroms: graphicUtil.Path | graphicUtil.Path[]
): Element {
// [Rule]
// If `renderItem` returns `null`/`undefined`/`false`, remove the previous el if existing.
@@ -1557,10 +1632,10 @@ function createOrUpdateItem(
// If `elOption` is `null`/`undefined`/`false` (when `renderItem` returns nothing).
if (!elOption) {
- el && group.remove(el);
+ removeElementDirectly(el, group);
return;
}
- el = doCreateOrUpdateEl(el, dataIndex, elOption, seriesModel, group, true);
+ el = doCreateOrUpdateEl(el, dataIndex, elOption, seriesModel, group, true, morphingFroms);
el && data.setItemGraphicEl(dataIndex, el);
enableHoverEmphasis(el, elOption.focus, elOption.blurScope);
@@ -1568,13 +1643,12 @@ function createOrUpdateItem(
return el;
}
-function applyShapeMorphingAnimation(oldEl: Element, el: Element, seriesModel: SeriesModel, dataIndex: number) {
- if (!((oldEl instanceof graphicUtil.Path) && (el instanceof graphicUtil.Path))) {
- if (__DEV__) {
- error('`morph` can only be applied on two paths.');
- }
- return;
- }
+function applyShapeMorphingAnimation(
+ oldEl: graphicUtil.Path | graphicUtil.Path[],
+ el: graphicUtil.Path,
+ seriesModel: SeriesModel,
+ dataIndex: number
+) {
if (seriesModel.isAnimationEnabled()) {
const duration = seriesModel.get('animationDurationUpdate');
const delay = seriesModel.get('animationDelayUpdate');
@@ -1590,13 +1664,20 @@ function applyShapeMorphingAnimation(oldEl: Element, el: Element, seriesModel: S
}
}
+function hintInvalidMorph(): void {
+ if (__DEV__) {
+ error('`morph` can only be applied on two paths.');
+ }
+}
+
function doCreateOrUpdateEl(
el: Element,
dataIndex: number,
elOption: CustomElementOption,
seriesModel: CustomSeriesModel,
group: ViewRootGroup,
- isRoot: boolean
+ isRoot: boolean,
+ morphingFroms: graphicUtil.Path | graphicUtil.Path[]
): Element {
if (__DEV__) {
@@ -1604,17 +1685,29 @@ function doCreateOrUpdateEl(
}
let toBeReplacedIdx = -1;
- let oldEl: Element;
+ let morphingFromEl: graphicUtil.Path | graphicUtil.Path[];
+ const optionMorph = (elOption as CustomZRPathOption).morph;
- if (el && doesElNeedRecreate(el, elOption)) {
- // Should keep at the original index, otherwise "merge by index" will be incorrect.
- toBeReplacedIdx = group.childrenRef().indexOf(el);
- oldEl = el;
- el = null;
+ if (el) {
+ const elNeedRecreate = doesElNeedRecreate(el, elOption);
+ if (optionMorph) {
+ if (!elCanMorph(el)) {
+ hintInvalidMorph();
+ }
+ // When an el has been morphing, we also need to make it go through
+ // morphing logic, other it will blink.
+ else if (elNeedRecreate || isPathMorphing(el)) {
+ morphingFromEl = el;
+ }
+ }
+ if (elNeedRecreate) {
+ // Should keep at the original index, otherwise "merge by index" will be incorrect.
+ toBeReplacedIdx = group.childrenRef().indexOf(el);
+ el = null;
+ }
}
- let isInit = !el;
- let needsMorphing = false;
+ const elIsNewCreated = !el;
if (!el) {
el = createEl(elOption);
@@ -1626,13 +1719,13 @@ function doCreateOrUpdateEl(
el.clearStates();
}
- // Do shape morphing
- if ((elOption as CustomZRPathOption).morph && el && oldEl && (el !== oldEl)) {
- needsMorphing = true;
- // Use update animation when morph is enabled.
- isInit = false;
+ if (morphingFroms && optionMorph) {
+ morphingFromEl = morphingFroms;
}
+ // Use update animation when morph is enabled.
+ const isInit = elIsNewCreated && !morphingFromEl;
+
attachedTxInfoTmp.normal.cfg = attachedTxInfoTmp.normal.conOpt =
attachedTxInfoTmp.emphasis.cfg = attachedTxInfoTmp.emphasis.conOpt =
attachedTxInfoTmp.blur.cfg = attachedTxInfoTmp.blur.conOpt =
@@ -1650,7 +1743,7 @@ function doCreateOrUpdateEl(
updateElNormal(
el,
- needsMorphing ? oldEl : null,
+ morphingFromEl,
dataIndex,
elOption,
elOption.style,
@@ -1945,7 +2038,8 @@ function mergeChildren(
newChildren[index],
seriesModel,
el,
- false
+ false,
+ null
);
}
for (let i = el.childCount() - 1; i >= index; i--) {
@@ -1997,7 +2091,8 @@ function processAddUpdate(
childOption,
context.seriesModel,
context.group,
- false
+ false,
+ null
);
}
@@ -2036,3 +2131,10 @@ function hasOwnPathData(shape: CustomSVGPathOption['shape']): boolean {
return shape && (hasOwn(shape, 'pathData') || hasOwn(shape, 'd'));
}
+function elCanMorph(el: Element): el is graphicUtil.Path {
+ return el && el instanceof graphicUtil.Path;
+}
+
+function removeElementDirectly(el: Element, group: ViewRootGroup): void {
+ el && group.remove(el);
+}
diff --git a/src/coord/scaleRawExtentInfo.ts b/src/coord/scaleRawExtentInfo.ts
index 9f90b1a..7b3c92d 100644
--- a/src/coord/scaleRawExtentInfo.ts
+++ b/src/coord/scaleRawExtentInfo.ts
@@ -286,12 +286,12 @@ const DATA_MIN_MAX_ATTR = { min: '_dataMin', max: '_dataMax' } as const;
* Scale extent info probably be required multiple times during a workflow.
* For example:
* (1) `dataZoom` depends it to get the axis extent in "100%" state.
- * (2) `processor/extentCalculator` depends it to make sure whethe axis extent is specified.
+ * (2) `processor/extentCalculator` depends it to make sure whether axis extent is specified.
* (3) `coordSys.update` use it to finally decide the scale extent.
- * But the callback of `min`/`max` should not be called multiple time.
- * The code should not be duplicated either.
- * So we cache the result in the scale instance, which will be recreated in the begining
- * of the workflow.
+ * But the callback of `min`/`max` should not be called multiple times.
+ * The code below should not be implemented repeatedly either.
+ * So we cache the result in the scale instance, which will be recreated at the begining
+ * of the workflow (because `scale` instance will be recreated each round of the workflow).
*/
export function ensureScaleRawExtentInfo(
scale: Scale,
diff --git a/src/data/DataDiffer.ts b/src/data/DataDiffer.ts
index 993ead1..43baad1 100644
--- a/src/data/DataDiffer.ts
+++ b/src/data/DataDiffer.ts
@@ -20,28 +20,51 @@
import {ArrayLike} from 'zrender/src/core/types';
// return key.
-export type DiffKeyGetter = (this: DataDiffer, value: unknown, index: number) => string;
-export type DiffCallbackAdd = (newIndex: number) => void;
-export type DiffCallbackUpdate = (newIndex: number, oldIndex: number) => void;
-export type DiffCallbackRemove = (oldIndex: number) => void;
+type DiffKeyGetter<CTX = unknown> =
+ (this: DataDiffer<CTX>, value: unknown, index: number) => string;
+type DiffCallbackAdd = (newIndex: number) => void;
+type DiffCallbackUpdate = (newIndex: number, oldIndex: number) => void;
+type DiffCallbackRemove = (oldIndex: number) => void;
+type DiffCallbackUpdateManyToOne = (newIndex: number, oldIndex: number[]) => void;
+type DiffCallbackUpdateOneToMany = (newIndex: number[], oldIndex: number) => void;
+
+/**
+ * The value of `DataIndexMap` can only be:
+ * + a number
+ * + a number[] that length >= 2.
+ * + null/undefined
+ */
type DataIndexMap = {[key: string]: number | number[]};
+function dataIndexMapValueLength(
+ valNumOrArrLengthMoreThan2: number | number[]
+): number {
+ return valNumOrArrLengthMoreThan2 == null
+ ? 0
+ : ((valNumOrArrLengthMoreThan2 as number[]).length || 1);
+}
+
function defaultKeyGetter(item: string): string {
return item;
}
-class DataDiffer<Ctx = unknown> {
+export type DataDiffCallbackMode = 'single' | 'multiple';
+
+class DataDiffer<CTX = unknown> {
private _old: ArrayLike<unknown>;
private _new: ArrayLike<unknown>;
- private _oldKeyGetter: DiffKeyGetter;
- private _newKeyGetter: DiffKeyGetter;
+ private _oldKeyGetter: DiffKeyGetter<CTX>;
+ private _newKeyGetter: DiffKeyGetter<CTX>;
private _add: DiffCallbackAdd;
private _update: DiffCallbackUpdate;
+ private _updateManyToOne: DiffCallbackUpdateManyToOne;
+ private _updateOneToMany: DiffCallbackUpdateOneToMany;
private _remove: DiffCallbackRemove;
+ private _cbModeMultiple: boolean;
- readonly context: Ctx;
+ readonly context: CTX;
/**
* @param context Can be visited by this.context in callback.
@@ -49,9 +72,10 @@ class DataDiffer<Ctx = unknown> {
constructor(
oldArr: ArrayLike<unknown>,
newArr: ArrayLike<unknown>,
- oldKeyGetter?: DiffKeyGetter,
- newKeyGetter?: DiffKeyGetter,
- context?: Ctx
+ oldKeyGetter?: DiffKeyGetter<CTX>,
+ newKeyGetter?: DiffKeyGetter<CTX>,
+ context?: CTX,
+ cbMode?: DataDiffCallbackMode
) {
this._old = oldArr;
this._new = newArr;
@@ -61,12 +85,14 @@ class DataDiffer<Ctx = unknown> {
// Visible in callback via `this.context`;
this.context = context;
+
+ this._cbModeMultiple = cbMode === 'multiple';
}
/**
* Callback function when add a data
*/
- add(func: DiffCallbackAdd): DataDiffer {
+ add(func: DiffCallbackAdd): this {
this._add = func;
return this;
}
@@ -74,94 +100,200 @@ class DataDiffer<Ctx = unknown> {
/**
* Callback function when update a data
*/
- update(func: DiffCallbackUpdate): DataDiffer {
+ update(func: DiffCallbackUpdate): this {
this._update = func;
return this;
}
/**
+ * Callback function when update a data and only work in `cbMode: 'byKey'`.
+ */
+ updateManyToOne(func: DiffCallbackUpdateManyToOne): this {
+ this._updateManyToOne = func;
+ return this;
+ }
+
+ /**
+ * Callback function when update a data and only work in `cbMode: 'byKey'`.
+ */
+ updateOneToMany(func: DiffCallbackUpdateOneToMany): this {
+ this._updateOneToMany = func;
+ return this;
+ }
+
+ /**
* Callback function when remove a data
*/
- remove(func: DiffCallbackRemove): DataDiffer {
+ remove(func: DiffCallbackRemove): this {
this._remove = func;
return this;
}
execute(): void {
+ this[this._cbModeMultiple ? '_executeByKey' : '_executeByIndex']();
+ }
+
+ private _executeByIndex(): void {
const oldArr = this._old;
const newArr = this._new;
-
- const oldDataIndexMap: DataIndexMap = {};
const newDataIndexMap: DataIndexMap = {};
- const oldDataKeyArr: string[] = [];
- const newDataKeyArr: string[] = [];
- let i;
+ const oldDataKeyArr: string[] = new Array(oldArr.length);
+ const newDataKeyArr: string[] = new Array(newArr.length);
- this._initIndexMap(oldArr, oldDataIndexMap, oldDataKeyArr, '_oldKeyGetter');
+ this._initIndexMap(oldArr, null, oldDataKeyArr, '_oldKeyGetter');
this._initIndexMap(newArr, newDataIndexMap, newDataKeyArr, '_newKeyGetter');
- for (i = 0; i < oldArr.length; i++) {
- const key = oldDataKeyArr[i];
- let idx = newDataIndexMap[key];
+ for (let i = 0; i < oldArr.length; i++) {
+ const oldKey = oldDataKeyArr[i];
+ const newIdxMapVal = newDataIndexMap[oldKey];
+ const newIdxMapValLen = dataIndexMapValueLength(newIdxMapVal);
// idx can never be empty array here. see 'set null' logic below.
- if (idx != null) {
+ if (newIdxMapValLen > 1) {
// Consider there is duplicate key (for example, use dataItem.name as key).
// We should make sure every item in newArr and oldArr can be visited.
- const len = (idx as number[]).length;
- if (len) {
- len === 1 && (newDataIndexMap[key] = null);
- idx = (idx as number[]).shift();
- }
- else {
- newDataIndexMap[key] = null;
+ const newIdx = (newIdxMapVal as number[]).shift();
+ if ((newIdxMapVal as number[]).length === 1) {
+ newDataIndexMap[oldKey] = (newIdxMapVal as number[])[0];
}
- this._update && this._update(idx as number, i);
+ this._update && this._update(newIdx as number, i);
+ }
+ else if (newIdxMapValLen === 1) {
+ newDataIndexMap[oldKey] = null;
+ this._update && this._update(newIdxMapVal as number, i);
}
else {
this._remove && this._remove(i);
}
}
- for (i = 0; i < newDataKeyArr.length; i++) {
- const key = newDataKeyArr[i];
- if (newDataIndexMap.hasOwnProperty(key)) {
- const idx = newDataIndexMap[key];
- if (idx == null) {
- continue;
- }
- // idx can never be empty array here. see 'set null' logic above.
- if (!(idx as number[]).length) {
- this._add && this._add(idx as number);
+ this._performRestAdd(newDataKeyArr, newDataIndexMap);
+ }
+
+ /**
+ * For example, consider the case:
+ * oldData: [o0, o1, o2, o3, o4, o5, o6, o7],
+ * newData: [n0, n1, n2, n3, n4, n5, n6, n7, n8],
+ * Where:
+ * o0, o1, n0 has key 'a' (many to one)
+ * o5, n4, n5, n6 has key 'b' (one to many)
+ * o2, n1 has key 'c' (one to one)
+ * n2, n3 has key 'd' (add)
+ * o3, o4 has key 'e' (remove)
+ * o6, o7, n7, n8 has key 'f' (many to many, treated as add and remove)
+ * Then:
+ * (The order of the following directives are not ensured.)
+ * this._updateManyToOne(n0, [o0, o1]);
+ * this._updateOneToMany([n4, n5, n6], o5);
+ * this._update(n1, o2);
+ * this._remove(o3);
+ * this._remove(o4);
+ * this._remove(o6);
+ * this._remove(o7);
+ * this._add(n2);
+ * this._add(n3);
+ * this._add(n7);
+ * this._add(n8);
+ */
+ private _executeByKey(): void {
+ const oldArr = this._old;
+ const newArr = this._new;
+ const oldDataIndexMap: DataIndexMap = {};
+ const newDataIndexMap: DataIndexMap = {};
+ const oldDataKeyArr: string[] = [];
+ const newDataKeyArr: string[] = [];
+
+ this._initIndexMap(oldArr, oldDataIndexMap, oldDataKeyArr, '_oldKeyGetter');
+ this._initIndexMap(newArr, newDataIndexMap, newDataKeyArr, '_newKeyGetter');
+
+ for (let i = 0; i < oldDataKeyArr.length; i++) {
+ const oldKey = oldDataKeyArr[i];
+ const oldIdxMapVal = oldDataIndexMap[oldKey];
+ const newIdxMapVal = newDataIndexMap[oldKey];
+ const oldIdxMapValLen = dataIndexMapValueLength(oldIdxMapVal);
+ const newIdxMapValLen = dataIndexMapValueLength(newIdxMapVal);
+
+ if (oldIdxMapValLen > 1 && newIdxMapValLen === 1) {
+ this._updateManyToOne && this._updateManyToOne(newIdxMapVal as number, oldIdxMapVal as number[]);
+ newDataIndexMap[oldKey] = null;
+ }
+ else if (oldIdxMapValLen === 1 && newIdxMapValLen > 1) {
+ this._updateOneToMany && this._updateOneToMany(newIdxMapVal as number[], oldIdxMapVal as number);
+ newDataIndexMap[oldKey] = null;
+ }
+ else if (oldIdxMapValLen === 1 && newIdxMapValLen === 1) {
+ this._update && this._update(newIdxMapVal as number, oldIdxMapVal as number);
+ newDataIndexMap[oldKey] = null;
+ }
+ else if (oldIdxMapValLen > 1) {
+ for (let i = 0; i < oldIdxMapValLen; i++) {
+ this._remove && this._remove((oldIdxMapVal as number[])[i]);
}
- else {
- for (let j = 0, len = (idx as number[]).length; j < len; j++) {
- this._add && this._add((idx as number[])[j]);
- }
+ }
+ else {
+ this._remove && this._remove(oldIdxMapVal as number);
+ }
+ }
+
+ this._performRestAdd(newDataKeyArr, newDataIndexMap);
+ }
+
+ private _performRestAdd(newDataKeyArr: string[], newDataIndexMap: DataIndexMap) {
+ for (let i = 0; i < newDataKeyArr.length; i++) {
+ const newKey = newDataKeyArr[i];
+ const newIdxMapVal = newDataIndexMap[newKey];
+ const idxMapValLen = dataIndexMapValueLength(newIdxMapVal);
+ if (idxMapValLen > 1) {
+ for (let j = 0; j < idxMapValLen; j++) {
+ this._add && this._add((newIdxMapVal as number[])[j]);
}
}
+ else if (idxMapValLen === 1) {
+ this._add && this._add(newIdxMapVal as number);
+ }
+ // Support both `newDataKeyArr` are duplication removed or not removed.
+ newDataIndexMap[newKey] = null;
}
}
private _initIndexMap(
arr: ArrayLike<unknown>,
+ // Can be null.
map: DataIndexMap,
+ // In 'byKey', the output `keyArr` is duplication removed.
+ // In 'byIndex', the output `keyArr` is not duplication removed and
+ // its indices are accurately corresponding to `arr`.
keyArr: string[],
keyGetterName: '_oldKeyGetter' | '_newKeyGetter'
): void {
+ const cbModeByKey = this._cbModeMultiple;
+
for (let i = 0; i < arr.length; i++) {
// Add prefix to avoid conflict with Object.prototype.
const key = '_ec_' + this[keyGetterName](arr[i], i);
- let existence = map[key];
- if (existence == null) {
- keyArr.push(key);
+ if (!cbModeByKey) {
+ keyArr[i] = key;
+ }
+ if (!map) {
+ continue;
+ }
+
+ const idxMapVal = map[key];
+ const idxMapValLen = dataIndexMapValueLength(idxMapVal);
+
+ if (idxMapValLen === 0) {
+ // Simple optimize: in most cases, one index has one key,
+ // do not need array.
map[key] = i;
+ if (cbModeByKey) {
+ keyArr.push(key);
+ }
+ }
+ else if (idxMapValLen === 1) {
+ map[key] = [idxMapVal as number, i];
}
else {
- if (!(existence as number[]).length) {
- map[key] = existence = [existence as number];
- }
- (existence as number[]).push(i);
+ (idxMapVal as number[]).push(i);
}
}
}
diff --git a/src/echarts.ts b/src/echarts.ts
index dbe4378..c27a65b 100644
--- a/src/echarts.ts
+++ b/src/echarts.ts
@@ -88,14 +88,15 @@ import {
ComponentMainType,
ComponentSubType,
ColorString,
- SelectChangedPayload
+ SelectChangedPayload,
+ DimensionLoose
} from './util/types';
import Displayable from 'zrender/src/graphic/Displayable';
import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable';
import { seriesSymbolTask, dataSymbolTask } from './visual/symbol';
import { getVisualFromData, getItemVisualFromData } from './visual/helper';
import LabelManager from './label/LabelManager';
-import { deprecateLog } from './util/log';
+import { deprecateLog, throwError } from './util/log';
import { handleLegacySelectEvents } from './legacy/dataSelectAction';
// At least canvas renderer.
@@ -185,9 +186,19 @@ interface SetOptionOpts {
silent?: boolean;
// Rule: only `id` mapped will be merged,
// other components of the certain `mainType` will be removed.
- replaceMerge?: GlobalModelSetOptionOpts['replaceMerge']
+ replaceMerge?: GlobalModelSetOptionOpts['replaceMerge'];
+ transition?: SetOptionTransitionOptItem | SetOptionTransitionOptItem[];
};
+interface SetOptionTransitionOptItem {
+ from: SetOptionTransitionOptFinder | DimensionLoose;
+ to: SetOptionTransitionOptFinder | DimensionLoose;
+}
+interface SetOptionTransitionOptFinder extends modelUtil.ModelFinderObject {
+ dimension: DimensionLoose;
+}
+
+
type EventMethodName = 'on' | 'off';
function createRegisterEventWithLowercaseECharts(method: EventMethodName) {
return function (this: ECharts, ...args: any): ECharts {
@@ -254,6 +265,10 @@ let renderSeries: (
let performPostUpdateFuncs: (ecModel: GlobalModel, api: ExtensionAPI) => void;
let createExtensionAPI: (ecIns: ECharts) => ExtensionAPI;
let enableConnect: (ecIns: ECharts) => void;
+let setTransitionOpt: (
+ chart: ECharts,
+ transitionOpt: SetOptionTransitionOptItem | SetOptionTransitionOptItem[]
+) => void;
let markStatusToUpdate: (ecIns: ECharts) => void;
let applyChangedStates: (ecIns: ECharts) => void;
@@ -309,6 +324,7 @@ class ECharts extends Eventful {
private _labelManager: LabelManager;
+
private [OPTION_UPDATED_KEY]: boolean | {silent: boolean};
private [IN_MAIN_PROCESS_KEY]: boolean;
private [CONNECT_STATUS_KEY]: ConnectStatus;
@@ -502,10 +518,12 @@ class ECharts extends Eventful {
let silent;
let replaceMerge;
+ let transitionOpt;
if (isObject(notMerge)) {
lazyUpdate = notMerge.lazyUpdate;
silent = notMerge.silent;
replaceMerge = notMerge.replaceMerge;
+ transitionOpt = notMerge.transition;
notMerge = notMerge.notMerge;
}
@@ -519,7 +537,9 @@ class ECharts extends Eventful {
ecModel.init(null, null, null, theme, this._locale, optionManager);
}
- this._model.setOption(option as ECOption, {replaceMerge: replaceMerge}, optionPreprocessorFuncs);
+ this._model.setOption(option as ECOption, { replaceMerge }, optionPreprocessorFuncs);
+
+ setTransitionOpt(this, transitionOpt);
if (lazyUpdate) {
this[OPTION_UPDATED_KEY] = {silent: silent};
@@ -852,7 +872,7 @@ class ECharts extends Eventful {
const ecModel = this._model;
const parsedFinder = modelUtil.parseFinder(ecModel, finder, {
- defaultMainType: 'series'
+ useDefaultMainType: ['series']
});
const seriesModel = parsedFinder.seriesModel;
@@ -1926,6 +1946,8 @@ class ECharts extends Eventful {
unfinished = true;
}
+ seriesModel.__transientTransitionOpt = null;
+
chartView.group.silent = !!seriesModel.get('silent');
// Should not call markRedraw on group, because it will disable zrender
// increamental render (alway render from the __startIndex each frame)
@@ -2260,6 +2282,70 @@ class ECharts extends Eventful {
});
};
+ setTransitionOpt = function (
+ chart: ECharts,
+ transitionOpt: SetOptionTransitionOptItem | SetOptionTransitionOptItem[]
+ ): void {
+ const ecModel = chart._model;
+ zrUtil.each(modelUtil.normalizeToArray(transitionOpt), transOpt => {
+
+ function normalizeFromTo(fromTo: DimensionLoose | SetOptionTransitionOptFinder) {
+ return (zrUtil.isString(fromTo) || zrUtil.isNumber(fromTo))
+ ? { dimension: fromTo }
+ : fromTo;
+ }
+
+ let errMsg;
+ const fromOpt = normalizeFromTo(transOpt.from);
+ const toOpt = normalizeFromTo(transOpt.to);
+
+ if (fromOpt == null || toOpt == null) {
+ if (__DEV__) {
+ errMsg = '`transition.from` and `transition.to` must be specified.';
+ }
+ throwError(errMsg);
+ }
+
+ const finderOpt = {
+ useDefaultMainType: ['series'],
+ includeMainTypes: ['series'],
+ enableAll: false,
+ enableNone: false
+ };
+ const fromResult = modelUtil.parseFinder(ecModel, fromOpt, finderOpt);
+ const toResult = modelUtil.parseFinder(ecModel, toOpt, finderOpt);
+ const toSeries = toResult.seriesModel;
+
+ if (toSeries == null) {
+ errMsg = '';
+ if (__DEV__) {
+ errMsg = '`transition` is only supported on series.';
+ }
+ }
+ if (fromResult.seriesModel !== toSeries) {
+ errMsg = '';
+ if (__DEV__) {
+ errMsg = '`transition.from` and `transition.to` must be specified to the same series.';
+ }
+ }
+ if (fromOpt.dimension == null || toOpt.dimension == null) {
+ errMsg = '';
+ if (__DEV__) {
+ errMsg = '`dimension` must be specified in `transition`.';
+ }
+ }
+ if (errMsg != null) {
+ throwError(errMsg);
+ }
+
+ // Just a temp solution: mount them on series.
+ toSeries.__transientTransitionOpt = {
+ from: fromOpt.dimension,
+ to: toOpt.dimension
+ };
+ });
+ };
+
})();
}
diff --git a/src/model/Global.ts b/src/model/Global.ts
index b1f5dfb..ec7927c 100644
--- a/src/model/Global.ts
+++ b/src/model/Global.ts
@@ -141,7 +141,7 @@ class GlobalModel extends Model<ECUnitOption> {
'please use chart.getOption()'
);
- const innerOpt = normalizeReplaceMergeInput(opts);
+ const innerOpt = normalizeSetOptionInput(opts);
this._optionManager.setOption(option, optionPreprocessorFuncs, innerOpt);
@@ -157,9 +157,9 @@ class GlobalModel extends Model<ECUnitOption> {
*/
resetOption(
type: 'recreate' | 'timeline' | 'media',
- opt?: GlobalModelSetOptionOpts
+ opt?: Pick<GlobalModelSetOptionOpts, 'replaceMerge'>
): boolean {
- return this._resetOption(type, normalizeReplaceMergeInput(opt));
+ return this._resetOption(type, normalizeSetOptionInput(opt));
}
private _resetOption(
@@ -933,7 +933,7 @@ function filterBySubType(
: components;
}
-function normalizeReplaceMergeInput(opts: GlobalModelSetOptionOpts): InnerSetOptionOpts {
+function normalizeSetOptionInput(opts: GlobalModelSetOptionOpts): InnerSetOptionOpts {
const replaceMergeMainTypeMap = createHashMap<boolean, string>();
opts && each(modelUtil.normalizeToArray(opts.replaceMerge), function (mainType) {
if (__DEV__) {
diff --git a/src/model/Series.ts b/src/model/Series.ts
index a082994..3a0f774 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
+ ScaleDataValue, Dictionary, OptionDataItemObject, SeriesDataType, DimensionLoose
} from '../util/types';
import ComponentModel, { ComponentModelConstructor } from './Component';
import {ColorPaletteMixin} from './mixin/colorPalette';
@@ -130,6 +130,15 @@ class SeriesModel<Opt extends SeriesOption = SeriesOption> extends ComponentMode
// Injected outside
pipelineContext: PipelineContext;
+ // only avalible in `render()` caused by `setOption`.
+ __transientTransitionOpt: {
+ // [MEMO] Currently only support single "from". If intending to
+ // support multiple "from", if not hard to implement "merge morph",
+ // but correspondingly not easy to implement "split morph".
+ from: DimensionLoose;
+ to: DimensionLoose;
+ };
+
// ---------------------------------------
// Props to tell visual/style.ts about how to do visual encoding.
// ---------------------------------------
diff --git a/src/util/model.ts b/src/util/model.ts
index c09a154..d7e46e8 100644
--- a/src/util/model.ts
+++ b/src/util/model.ts
@@ -27,7 +27,9 @@ import {
assert,
isString,
indexOf,
- isStringSafe
+ isStringSafe,
+ hasOwn,
+ defaults
} from 'zrender/src/core/util';
import env from 'zrender/src/core/env';
import GlobalModel from '../model/Global';
@@ -750,6 +752,7 @@ let innerUniqueIndex = getRandomIdBase();
export type ModelFinderIndexQuery = number | number[] | 'all' | 'none' | false;
export type ModelFinderIdQuery = OptionId | OptionId[];
export type ModelFinderNameQuery = OptionId | OptionId[];
+// If string, like 'series', means { seriesIndex: 0 }.
export type ModelFinder = string | ModelFinderObject;
export type ModelFinderObject = {
seriesIndex?: ModelFinderIndexQuery, seriesId?: ModelFinderIdQuery, seriesName?: ModelFinderNameQuery
@@ -793,7 +796,13 @@ export type ParsedModelFinder = ParsedModelFinderKnown & {
export function parseFinder(
ecModel: GlobalModel,
finderInput: ModelFinder,
- opt?: {defaultMainType?: ComponentMainType, includeMainTypes?: ComponentMainType[]}
+ opt?: {
+ includeMainTypes?: ComponentMainType[];
+ // The `mainType` listed will set `useDefault: true`.
+ useDefaultMainType?: ComponentMainType[];
+ enableAll?: boolean;
+ enableNone?: boolean;
+ }
): ParsedModelFinder {
let finder: ModelFinderObject;
if (isString(finderInput)) {
@@ -805,7 +814,6 @@ export function parseFinder(
finder = finderInput;
}
- const defaultMainType = opt ? opt.defaultMainType : null;
const queryOptionMap = createHashMap<QueryReferringUserOption, ComponentMainType>();
const result = {} as ParsedModelFinder;
@@ -823,7 +831,6 @@ export function parseFinder(
if (
!mainType
|| !queryType
- || (mainType !== defaultMainType && value == null)
|| (opt && opt.includeMainTypes && indexOf(opt.includeMainTypes, mainType) < 0)
) {
return;
@@ -833,15 +840,20 @@ export function parseFinder(
queryOption[queryType] = value as any;
});
+ const useDefaultMainType = opt && opt.useDefaultMainType || [];
+ each(useDefaultMainType, function (mainType) {
+ !queryOptionMap.get(mainType) && queryOptionMap.set(mainType, {});
+ });
+
queryOptionMap.each(function (queryOption, mainType) {
const queryResult = queryReferringComponents(
ecModel,
mainType,
queryOption,
{
- useDefault: mainType === defaultMainType,
- enableAll: true,
- enableNone: true
+ useDefault: indexOf(useDefaultMainType, mainType) >= 0,
+ enableAll: (opt && opt.enableAll != null) ? opt.enableAll : true,
+ enableNone: (opt && opt.enableNone != null) ? opt.enableNone : true
}
);
result[mainType + 'Models'] = queryResult.models;
diff --git a/src/util/types.ts b/src/util/types.ts
index d57e6b3..dbe4d21 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -46,6 +46,7 @@ import { PathStyleProps } from 'zrender/src/graphic/Path';
import { ImageStyleProps } from 'zrender/src/graphic/Image';
import ZRText, { TextStyleProps } from 'zrender/src/graphic/Text';
import { Source } from '../data/Source';
+import { ModelFinderObject } from './model';
@@ -237,13 +238,42 @@ export interface StageHandlerOverallReset {
(ecModel: GlobalModel, api: ExtensionAPI, payload?: Payload): void
}
export interface StageHandler {
- seriesType?: string;
+ /**
+ * Indicate that the task will be only piped all series
+ * (`performRawSeries` indicate whether includes filtered series).
+ */
createOnAllSeries?: boolean;
+ /**
+ * Indicate that the task will be only piped in the pipeline of this type of series.
+ * (`performRawSeries` indicate whether includes filtered series).
+ */
+ seriesType?: string;
+ /**
+ * Indicate that the task will be only piped in the pipeline of the returned series.
+ */
+ getTargetSeries?: (ecModel: GlobalModel, api: ExtensionAPI) => HashMap<SeriesModel>;
+
+ /**
+ * If `true`, filtered series will also be "performed".
+ */
performRawSeries?: boolean;
+
+ /**
+ * Called only when this task in a pipeline.
+ */
plan?: StageHandlerPlan;
+ /**
+ * If `overallReset` specified, an "overall task" will be created.
+ * "overall task" does not belong to a certain pipeline.
+ * They always be "performed" in certain phase (depends on when they declared).
+ * They has "stub"s to connect with pipelines (one stub for one pipeline),
+ * delivering info like "dirty" and "output end".
+ */
overallReset?: StageHandlerOverallReset;
+ /**
+ * Called only when this task in a pipeline, and "dirty".
+ */
reset?: StageHandlerReset;
- getTargetSeries?: (ecModel: GlobalModel, api: ExtensionAPI) => HashMap<SeriesModel>;
}
export interface StageHandlerInternal extends StageHandler {
diff --git a/test/custom-shape-morphing2.html b/test/custom-shape-morphing2.html
new file mode 100644
index 0000000..01e8bc2
--- /dev/null
+++ b/test/custom-shape-morphing2.html
@@ -0,0 +1,388 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <script src='lib/esl.js'></script>
+ <script src='lib/config.js'></script>
+ <script src='lib/jquery.min.js'></script>
+ <script src="lib/testHelper.js"></script>
+ <link rel="stylesheet" href="lib/reset.css" />
+ </head>
+ <body>
+ <style>
+ </style>
+
+ <div id="main0"></div>
+
+
+
+ <script>
+
+ require([
+ 'echarts'
+ ], function (echarts) {
+
+ const COLORS = [
+ '#37A2DA', '#e06343', '#37a354', '#b55dba', '#b5bd48', '#8378EA', '#96BFFF'
+ ];
+ var COUNT = 50;
+ var CONTENT_COLOR = '#37A2DA';
+
+ function makeRandomValue(range, precision) {
+ return +(
+ Math.random() * (range[1] - range[0]) + range[0]
+ ).toFixed(precision);
+ }
+
+ var M_TAG_LIST = ['MA', 'MB', 'MC', 'MD'];
+ var Z_TAG_LIST = ['ZA', 'ZB', 'ZC', 'ZD', 'ZE'];
+ var ANIMATION_DURATION_UPDATE = 1500;
+
+ function initRawData() {
+ var DIMENSION = {
+ DATE: 0,
+ ATA: 1,
+ STE: 2,
+ CTZ: 3,
+ M_TAG: 4,
+ Z_TAG: 5,
+ ID: 6
+ };
+ var data = [];
+ var currDate = +new Date(2015, 2, 1);
+ var ONE_DAY = 3600 * 24 * 1000;
+ for (var i = 0; i < COUNT; i++, currDate += ONE_DAY) {
+ var line = [];
+ data.push(line);
+ line[DIMENSION.DATE] = currDate;
+ line[DIMENSION.ATA] = makeRandomValue([10, 40], 0);
+ line[DIMENSION.STE] = makeRandomValue([0.01, 0.99], 2);
+ line[DIMENSION.CTZ] = makeRandomValue([1, 10], 1);
+ line[DIMENSION.M_TAG] = M_TAG_LIST[makeRandomValue([0, M_TAG_LIST.length - 1], 0)];
+ line[DIMENSION.Z_TAG] = Z_TAG_LIST[makeRandomValue([0, Z_TAG_LIST.length - 1], 0)];
+ line[DIMENSION.ID] = 'P' + i;
+ }
+ return {
+ DIMENSION: DIMENSION,
+ data: data
+ };
+ }
+ function aggregateSum(rawDataWrap, byDimProp, RESULT_DIMENSION) {
+ var map = {};
+ var result = [];
+ var data = rawDataWrap.data;
+
+ for (var i = 0; i < data.length; i++) {
+ var line = data[i];
+ var byVal = line[rawDataWrap.DIMENSION[byDimProp]];
+ if (!map.hasOwnProperty(byVal)) {
+ var newLine = [];
+ map[byVal] = newLine;
+ result.push(newLine);
+ newLine[RESULT_DIMENSION.ATA] = 0;
+ newLine[RESULT_DIMENSION.STE] = 0;
+ newLine[RESULT_DIMENSION.CTZ] = 0;
+ newLine[RESULT_DIMENSION[byDimProp]] = byVal;
+ }
+ else {
+ var newLine = map[byVal];
+ newLine[RESULT_DIMENSION.ATA] += line[rawDataWrap.DIMENSION.ATA];
+ newLine[RESULT_DIMENSION.STE] += line[rawDataWrap.DIMENSION.STE];
+ newLine[RESULT_DIMENSION.CTZ] += line[rawDataWrap.DIMENSION.CTZ];
+ }
+ }
+
+ return {
+ DIMENSION: RESULT_DIMENSION,
+ data: result,
+ uniqueKey: 'M_TAG'
+ };
+ }
+
+ var rawDataWrap = initRawData();
+ // console.log(JSON.stringify(rawDataWrap.data));
+ rawDataWrap.data = [[1425139200000,34,0.13,2,"MD","ZD","P0"],[1425225600000,28,0.71,1.5,"MB","ZD","P1"],[1425312000000,23,0.9,2.8,"MA","ZC","P2"],[1425398400000,21,0.58,6,"MB","ZC","P3"],[1425484800000,14,0.1,1.6,"MC","ZA","P4"],[1425571200000,21,0.6,7.7,"MC","ZA","P5"],[1425657600000,23,0.31,2.6,"MC","ZC","P6"],[1425744000000,34,0.74,2.4,"MD","ZE","P7"],[1425830400000,14,0.59,2.3,"MB","ZD","P8"],[1425916800000,18,0.85,5.1,"MB","ZB","P9"],[1426003200000,36,0.96,1.2,"MC"," [...]
+ var mTagSumDataWrap = aggregateSum(rawDataWrap, 'M_TAG', {
+ ATA: 0,
+ STE: 1,
+ CTZ: 2,
+ M_TAG: 3
+ });
+ var zTagSumDataWrap = aggregateSum(rawDataWrap, 'Z_TAG', {
+ ATA: 0,
+ STE: 1,
+ CTZ: 2,
+ Z_TAG: 3
+ });
+
+
+ function create_Scatter_ATA_STE() {
+ var option = {
+ tooltip: {},
+ grid: {
+ containLabel: true
+ },
+ xAxis: {
+ name: 'STE'
+ },
+ yAxis: {
+ name: 'ATA'
+ },
+ dataZoom: [{
+ type: 'slider',
+ }, {
+ type: 'inside'
+ }],
+ series: {
+ type: 'custom',
+ coordinateSystem: 'cartesian2d',
+ animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+ data: rawDataWrap.data,
+ encode: {
+ itemName: rawDataWrap.DIMENSION.ID,
+ x: rawDataWrap.DIMENSION.STE,
+ y: rawDataWrap.DIMENSION.ATA,
+ tooltip: [rawDataWrap.DIMENSION.STE, rawDataWrap.DIMENSION.ATA]
+ },
+ renderItem: function (params, api) {
+ var pos = api.coord([
+ api.value(rawDataWrap.DIMENSION.STE),
+ api.value(rawDataWrap.DIMENSION.ATA)
+ ]);
+ return {
+ type: 'circle',
+ // x: pos[0],
+ // y: pos[1],
+ morph: true,
+ shape: {
+ // cx: 0,
+ // cy: 0,
+ cx: pos[0],
+ cy: pos[1],
+ r: 10,
+ transition: ['cx', 'cy', 'r']
+ },
+ style: {
+ transition: 'lineWidth',
+ fill: CONTENT_COLOR,
+ stroke: '#555',
+ lineWidth: 1
+ }
+ };
+ }
+ }
+ };
+
+ return {
+ option: option,
+ dataWrap: rawDataWrap
+ };
+ }
+
+ function create_Bar_mSum_ATA(mTagSumDataWrap) {
+ var option = {
+ tooltip: {},
+ grid: {
+ containLabel: true
+ },
+ xAxis: {
+ type: 'category'
+ },
+ yAxis: {
+ },
+ dataZoom: [{
+ type: 'slider',
+ }, {
+ type: 'inside'
+ }],
+ series: {
+ type: 'custom',
+ coordinateSystem: 'cartesian2d',
+ animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+ data: mTagSumDataWrap.data,
+ encode: {
+ x: mTagSumDataWrap.DIMENSION.M_TAG,
+ y: mTagSumDataWrap.DIMENSION.ATA,
+ tooltip: [mTagSumDataWrap.DIMENSION.M_TAG, mTagSumDataWrap.DIMENSION.ATA]
+ },
+ renderItem: function (params, api) {
+ var mTagVal = api.value(mTagSumDataWrap.DIMENSION.M_TAG);
+ var ataVal = api.value(mTagSumDataWrap.DIMENSION.ATA);
+ var tarPos = api.coord([mTagVal, ataVal]);
+ var zeroPos = api.coord([mTagVal, 0]);
+ var size = api.size([mTagVal, ataVal]);
+ var width = size[0] * 0.4;
+ return {
+ type: 'rect',
+ morph: true,
+ shape: {
+ x: tarPos[0] - width / 2,
+ y: tarPos[1],
+ height: zeroPos[1] - tarPos[1],
+ width: width,
+ transition: ['x', 'y', 'width', 'height']
+ },
+ style: {
+ transition: 'lineWidth',
+ fill: CONTENT_COLOR,
+ stroke: '#555',
+ lineWidth: 0
+ }
+ };
+ }
+ }
+ };
+
+ return {
+ option: option,
+ dataWrap: mTagSumDataWrap
+ };
+ }
+
+ function create_Pie_mSum_ATA(mTagSumDataWrap) {
+ var totalValue = mTagSumDataWrap.data.reduce(function (val, item) {
+ return val + item[mTagSumDataWrap.DIMENSION.ATA];
+ }, 0);
+ let angles = [];
+ let currentAngle = -Math.PI / 2;
+ for (let i = 0; i < mTagSumDataWrap.data.length; i++) {
+ const angle = mTagSumDataWrap.data[i][mTagSumDataWrap.DIMENSION.ATA] / totalValue * Math.PI * 2;
+ angles.push([currentAngle, angle + currentAngle]);
+ currentAngle += angle;
+ }
+
+ var option = {
+ tooltip: {},
+ series: {
+ type: 'custom',
+ coordinateSystem: 'none',
+ animationDurationUpdate: ANIMATION_DURATION_UPDATE,
+ data: mTagSumDataWrap.data,
+ encode: {
+ itemName: mTagSumDataWrap.DIMENSION.M_TAG,
+ value: mTagSumDataWrap.DIMENSION.ATA,
+ tooltip: [mTagSumDataWrap.DIMENSION.ATA]
+ },
+ renderItem: function (params, api) {
+ const width = chart.getWidth();
+ const height = chart.getHeight();
+ return {
+ type: 'sector',
+ morph: true,
+ shape: {
+ cx: width / 2,
+ cy: height / 2,
+ r: Math.min(width, height) / 3,
+ r0: Math.min(width, height) / 5,
+ startAngle: angles[params.dataIndex][0],
+ endAngle: angles[params.dataIndex][1],
+ clockwise: true
+ },
+ style: {
+ fill: CONTENT_COLOR,
+ stroke: '#555',
+ strokeNoScale: true
+ },
+ };
+ }
+ }
+ };
+
+ return {
+ option: option,
+ dataWrap: mTagSumDataWrap
+ };
+ }
+
+ function createScatter_zSum_ATA(zTagSumDataWrap) {
+ }
+
+
+ var currOptionName = 'Scatter_ATA_STE';
+ var optionInfoList = {
+ 'Scatter_ATA_STE': create_Scatter_ATA_STE(rawDataWrap),
+ 'Bar_mTagSum_ATA': create_Bar_mSum_ATA(mTagSumDataWrap),
+ 'Pie_mTagSum_ATA': create_Pie_mSum_ATA(mTagSumDataWrap),
+ };
+
+ function next(nextOptionName) {
+ const lastOptionInfo = optionInfoList[currOptionName];
+ const nextOptionInfo = optionInfoList[nextOptionName];
+
+ const commonDimension = findCommonDimension(lastOptionInfo, nextOptionInfo)
+ || findCommonDimension(nextOptionInfo, lastOptionInfo);
+ const fromDimension = lastOptionInfo.dataWrap.DIMENSION[commonDimension];
+ const toDimension = nextOptionInfo.dataWrap.DIMENSION[commonDimension];
+ const transitionOpt = (fromDimension != null && toDimension != null)
+ ? { from: fromDimension, to: toDimension } : null;
+
+ currOptionName = nextOptionName;
+
+ chart.setOption(nextOptionInfo.option, {
+ replaceMerge: ['xAxis', 'yAxis'],
+ transition: transitionOpt
+ });
+ }
+
+ function findCommonDimension(optionInfoA, optionInfoB) {
+ var uniqueKey = optionInfoB.dataWrap.uniqueKey;
+ if (uniqueKey != null && optionInfoA.dataWrap.DIMENSION[uniqueKey] != null) {
+ return uniqueKey;
+ }
+ }
+
+ var chart = testHelper.create(echarts, 'main0', {
+ title: [
+ 'Test: buttons, should morph animation merge/split.',
+ 'Test: click buttons **before animation finished**, should no blink.',
+ 'Test: click buttons **twice**, should no blink.',
+ 'Test: use dataZoom, update animation should exist'
+ ],
+ option: optionInfoList[currOptionName].option,
+ height: 600,
+ buttons: [{
+ text: 'ToBar',
+ onclick: function () {
+ next('Bar_mTagSum_ATA');
+ }
+ }, {
+ text: 'ToScatter',
+ onclick: function () {
+ next('Scatter_ATA_STE');
+ }
+ }, {
+ text: 'ToPie',
+ onclick: function () {
+ next('Pie_mTagSum_ATA');
+ }
+ }]
+ });
+
+ });
+
+ </script>
+
+
+
+ </body>
+</html>
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org