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/07/17 11:39:01 UTC
[incubator-echarts] 02/16: feature: add `setOption` control param:
`replaceMerge`.
This is an automated email from the ASF dual-hosted git repository.
sushuang pushed a commit to branch remove-component
in repository https://gitbox.apache.org/repos/asf/incubator-echarts.git
commit 065cf80c1e7c34c2dec405e81e45f6f339b07e3e
Author: 100pah <su...@gmail.com>
AuthorDate: Tue Jul 7 23:02:06 2020 +0800
feature: add `setOption` control param: `replaceMerge`.
---
src/chart/helper/createListFromArray.ts | 2 +-
src/chart/treemap/TreemapSeries.ts | 3 +-
src/component/graphic.ts | 8 +-
src/component/timeline/TimelineModel.ts | 6 +-
src/component/timeline/timelineAction.ts | 2 +-
src/echarts.ts | 36 +-
src/model/Component.ts | 1 +
src/model/Global.ts | 396 ++++++++++++------
src/model/OptionManager.ts | 35 +-
src/util/model.ts | 268 +++++++++----
src/util/types.ts | 2 +-
test/lib/testHelper.js | 58 +++
test/option-replaceMerge.html | 661 +++++++++++++++++++++++++++++++
test/option-replaceMerge2.html | 497 +++++++++++++++++++++++
test/timeline-dynamic-series.html | 201 +++++-----
test/timeline-life.html | 279 +++++++++++++
16 files changed, 2101 insertions(+), 354 deletions(-)
diff --git a/src/chart/helper/createListFromArray.ts b/src/chart/helper/createListFromArray.ts
index d55bf3b..253240f 100644
--- a/src/chart/helper/createListFromArray.ts
+++ b/src/chart/helper/createListFromArray.ts
@@ -47,7 +47,7 @@ function createListFromArray(source: Source | any[], seriesModel: SeriesModel, o
let coordSysDimDefs: DimensionDefinitionLoose[];
- if (coordSysInfo) {
+ if (coordSysInfo && coordSysInfo.coordSysDims) {
coordSysDimDefs = zrUtil.map(coordSysInfo.coordSysDims, function (dim) {
const dimInfo = {
name: dim
diff --git a/src/chart/treemap/TreemapSeries.ts b/src/chart/treemap/TreemapSeries.ts
index 18b036b..f325f49 100644
--- a/src/chart/treemap/TreemapSeries.ts
+++ b/src/chart/treemap/TreemapSeries.ts
@@ -36,6 +36,7 @@ import {
import GlobalModel from '../../model/Global';
import { LayoutRect } from '../../util/layout';
import List from '../../data/List';
+import { normalizeToArray } from '../../util/model';
// Only support numberic value.
type TreemapSeriesDataValue = number | number[];
@@ -519,7 +520,7 @@ function completeTreeValue(dataNode: TreemapSeriesNodeItemOption) {
* set default to level configuration
*/
function setDefault(levels: TreemapSeriesLevelOption[], ecModel: GlobalModel) {
- const globalColorList = ecModel.get('color');
+ const globalColorList = normalizeToArray(ecModel.get('color')) as ColorString[];
if (!globalColorList) {
return;
diff --git a/src/component/graphic.ts b/src/component/graphic.ts
index 7bc0247..1996918 100644
--- a/src/component/graphic.ts
+++ b/src/component/graphic.ts
@@ -142,18 +142,18 @@ const GraphicModel = echarts.extendComponentModel({
const flattenedList = [];
this._flatten(newList, flattenedList);
- const mappingResult = modelUtil.mappingToExists(existList, flattenedList);
+ const mappingResult = modelUtil.mappingToExistsInNormalMerge(existList, flattenedList);
modelUtil.makeIdAndName(mappingResult);
// Clear elOptionsToUpdate
const elOptionsToUpdate = this._elOptionsToUpdate = [];
zrUtil.each(mappingResult, function (resultItem, index) {
- const newElOption = resultItem.option;
+ const newElOption = resultItem.newOption;
if (__DEV__) {
zrUtil.assert(
- zrUtil.isObject(newElOption) || resultItem.exist,
+ zrUtil.isObject(newElOption) || resultItem.existing,
'Empty graphic option definition'
);
}
@@ -502,7 +502,7 @@ function isSetLoc(obj, props) {
}
function setKeyInfoToNewElOption(resultItem, newElOption) {
- const existElOption = resultItem.exist;
+ const existElOption = resultItem.existing;
// Set id and type after id assigned.
newElOption.id = resultItem.keyInfo.id;
diff --git a/src/component/timeline/TimelineModel.ts b/src/component/timeline/TimelineModel.ts
index 11c9982..385854c 100644
--- a/src/component/timeline/TimelineModel.ts
+++ b/src/component/timeline/TimelineModel.ts
@@ -36,7 +36,7 @@ import {
ZREasing
} from '../../util/types';
import Model from '../../model/Model';
-import GlobalModel from '../../model/Global';
+import GlobalModel, { GlobalModelSetOptionOpts } from '../../model/Global';
import { each, isObject, clone, isString } from 'zrender/src/core/util';
@@ -121,6 +121,10 @@ export interface TimelineOption extends ComponentOption, BoxLayoutOptionMixin, S
inverse?: boolean
+ // If not specified, options will be changed by "normalMerge".
+ // If specified, options will be changed by "replaceMerge".
+ replaceMerge?: GlobalModelSetOptionOpts['replaceMerge']
+
lineStyle?: TimelineLineStyleOption
itemStyle?: ItemStyleOption
checkpointStyle?: TimelineCheckpointStyle
diff --git a/src/component/timeline/timelineAction.ts b/src/component/timeline/timelineAction.ts
index 4e219d4..6972844 100644
--- a/src/component/timeline/timelineAction.ts
+++ b/src/component/timeline/timelineAction.ts
@@ -48,7 +48,7 @@ echarts.registerAction(
}
// Set normalized currentIndex to payload.
- ecModel.resetOption('timeline');
+ ecModel.resetOption('timeline', { replaceMerge: timelineModel.get('replaceMerge', true) });
return defaults({
currentIndex: timelineModel.option.currentIndex
diff --git a/src/echarts.ts b/src/echarts.ts
index 765ee6a..515e041 100644
--- a/src/echarts.ts
+++ b/src/echarts.ts
@@ -27,7 +27,7 @@ import Eventful from 'zrender/src/core/Eventful';
import Element, { ElementEvent } from 'zrender/src/Element';
import CanvasPainter from 'zrender/src/canvas/Painter';
import SVGPainter from 'zrender/src/svg/Painter';
-import GlobalModel, {QueryConditionKindA} from './model/Global';
+import GlobalModel, {QueryConditionKindA, GlobalModelSetOptionOpts} from './model/Global';
import ExtensionAPI from './ExtensionAPI';
import CoordinateSystemManager from './CoordinateSystem';
import OptionManager from './model/OptionManager';
@@ -145,10 +145,13 @@ type ConnectStatus =
| typeof CONNECT_STATUS_UPDATING
| typeof CONNECT_STATUS_UPDATED;
-type SetOptionOpts = {
- notMerge?: boolean,
- lazyUpdate?: boolean,
- silent?: boolean
+interface SetOptionOpts {
+ notMerge?: boolean;
+ lazyUpdate?: boolean;
+ silent?: boolean;
+ // Rule: only `id` mapped will be merged,
+ // other components of the certain `mainType` will be removed.
+ replaceMerge?: GlobalModelSetOptionOpts['replaceMerge']
};
type EventMethodName = 'on' | 'off';
@@ -435,9 +438,10 @@ class ECharts extends Eventful {
* });
*
* @param opts opts or notMerge.
- * @param opts.notMerge Default `false`
+ * @param opts.notMerge Default `false`.
* @param opts.lazyUpdate Default `false`. Useful when setOption frequently.
* @param opts.silent Default `false`.
+ * @param opts.replaceMerge Default undefined.
*/
setOption(option: ECOption, notMerge?: boolean, lazyUpdate?: boolean): void;
setOption(option: ECOption, opts?: SetOptionOpts): void;
@@ -451,9 +455,11 @@ class ECharts extends Eventful {
}
let silent;
+ let replaceMerge;
if (isObject(notMerge)) {
lazyUpdate = notMerge.lazyUpdate;
silent = notMerge.silent;
+ replaceMerge = notMerge.replaceMerge;
notMerge = notMerge.notMerge;
}
@@ -467,7 +473,7 @@ class ECharts extends Eventful {
ecModel.init(null, null, null, theme, optionManager);
}
- this._model.setOption(option, optionPreprocessorFuncs);
+ this._model.setOption(option, {replaceMerge: replaceMerge}, optionPreprocessorFuncs);
if (lazyUpdate) {
this[OPTION_UPDATED] = {silent: silent};
@@ -1190,9 +1196,18 @@ class ECharts extends Eventful {
: ecModel.eachSeries(doPrepare);
function doPrepare(model: ComponentModel): void {
+ // By defaut view will be reused if possible for the case that `setOption` with "notMerge"
+ // mode and need to enable transition animation. (Usually, when they have the same id, or
+ // especially no id but have the same type & name & index. See the `model.id` generation
+ // rule in `makeIdAndName` and `viewId` generation rule here).
+ // But in `replaceMerge` mode, this feature should be able to disabled when it is clear that
+ // the new model has nothing to do with the old model.
+ const requireNewView = model.__requireNewView;
+ // This command should not work twice.
+ model.__requireNewView = false;
// Consider: id same and type changed.
const viewId = '_ec_' + model.id + '_' + model.type;
- let view = viewMap[viewId];
+ let view = !requireNewView && viewMap[viewId];
if (!view) {
const classType = parseClassType(model.type);
const Clazz = isComponent
@@ -1203,7 +1218,6 @@ class ECharts extends Eventful {
// For backward compat, still support a chart type declared as only subType
// like "liquidfill", but recommend "series.liquidfill"
// But need a base class to make a type series.
- // ||
(ChartView as ChartViewConstructor).getClass(classType.sub)
);
@@ -1237,7 +1251,9 @@ class ECharts extends Eventful {
zr.remove(view.group);
view.dispose(ecModel, api);
viewList.splice(i, 1);
- delete viewMap[view.__id];
+ if (viewMap[view.__id] === view) {
+ delete viewMap[view.__id];
+ }
view.__id = view.group.__ecComponentInfo = null;
}
else {
diff --git a/src/model/Component.ts b/src/model/Component.ts
index f686f0e..e9c89ce 100644
--- a/src/model/Component.ts
+++ b/src/model/Component.ts
@@ -128,6 +128,7 @@ class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Mode
// Injectable properties:
__viewId: string;
+ __requireNewView: boolean;
static protoInitialize = (function () {
const proto = ComponentModel.prototype;
diff --git a/src/model/Global.ts b/src/model/Global.ts
index 572927d..fe9edae 100644
--- a/src/model/Global.ts
+++ b/src/model/Global.ts
@@ -27,14 +27,16 @@
* (2) In `merge option` mode, if a component has no id/name specified, it
* will be merged by index, and the result sequence of the components is
* consistent to the original sequence.
- * (3) `reset` feature (in toolbox). Find detailed info in comments about
+ * (3) In `replaceMerge` mode, keep the result sequence of the components is
+ * consistent to the original sequence, even though there might result in "hole".
+ * (4) `reset` feature (in toolbox). Find detailed info in comments about
* `mergeOption` in module:echarts/model/OptionManager.
*/
import {__DEV__} from '../config';
import {
- each, filter, map, isArray, indexOf, isObject, isString,
- createHashMap, assert, clone, merge, extend, mixin, HashMap
+ each, filter, isArray, isObject, isString,
+ createHashMap, assert, clone, merge, extend, mixin, HashMap, isFunction
} from 'zrender/src/core/util';
import * as modelUtil from '../util/model';
import Model from './Model';
@@ -55,12 +57,18 @@ import {
} from '../util/types';
import OptionManager from './OptionManager';
import Scheduler from '../stream/Scheduler';
-import { Dictionary } from 'zrender/src/core/types';
+
+export interface GlobalModelSetOptionOpts {
+ replaceMerge: ComponentMainType | ComponentMainType[];
+}
+export interface InnerSetOptionOpts {
+ replaceMergeMainTypeMap: HashMap<boolean>;
+}
// -----------------------
// Internal method names:
// -----------------------
-let createSeriesIndices: (ecModel: GlobalModel, seriesModels: ComponentModel[]) => void;
+let reCreateSeriesIndices: (ecModel: GlobalModel) => void;
let assertSeriesInitialized: (ecModel: GlobalModel) => void;
let initBase: (ecModel: GlobalModel, baseOption: ECUnitOption) => void;
@@ -76,13 +84,23 @@ class GlobalModel extends Model<ECUnitOption> {
private _componentsMap: HashMap<ComponentModel[]>;
/**
+ * `_componentsMap` might have "hole" becuase of remove.
+ * So save components count for a certain mainType here.
+ */
+ private _componentsCount: HashMap<number>;
+
+ /**
* Mapping between filtered series list and raw series list.
* key: filtered series indices, value: raw series indices.
+ * Items of `_seriesIndices` never be null/empty/-1.
+ * If series has been removed by `replaceMerge`, those series
+ * also won't be in `_seriesIndices`, just like be filtered.
*/
private _seriesIndices: number[];
/**
- * Key: seriesIndex
+ * Key: seriesIndex.
+ * Keep consistent with `_seriesIndices`.
*/
private _seriesIndicesMap: HashMap<any>;
@@ -103,15 +121,21 @@ class GlobalModel extends Model<ECUnitOption> {
this._optionManager = optionManager;
}
- setOption(option: ECOption, optionPreprocessorFuncs: OptionPreprocessor[]): void {
+ setOption(
+ option: ECOption,
+ opts: GlobalModelSetOptionOpts,
+ optionPreprocessorFuncs: OptionPreprocessor[]
+ ): void {
assert(
!(OPTION_INNER_KEY in option),
'please use chart.getOption()'
);
- this._optionManager.setOption(option, optionPreprocessorFuncs);
+ const innerOpt = normalizeReplaceMergeInput(opts);
+
+ this._optionManager.setOption(option, optionPreprocessorFuncs, innerOpt);
- this.resetOption(null);
+ this._resetOption(null, innerOpt);
}
/**
@@ -121,7 +145,17 @@ class GlobalModel extends Model<ECUnitOption> {
* 'media': only reset media query option
* @return Whether option changed.
*/
- resetOption(type: string): boolean {
+ resetOption(
+ type: 'recreate' | 'timeline' | 'media',
+ opt?: GlobalModelSetOptionOpts
+ ): boolean {
+ return this._resetOption(type, normalizeReplaceMergeInput(opt));
+ }
+
+ private _resetOption(
+ type: 'recreate' | 'timeline' | 'media',
+ opt: InnerSetOptionOpts
+ ): boolean {
let optionChanged = false;
const optionManager = this._optionManager;
@@ -133,7 +167,7 @@ class GlobalModel extends Model<ECUnitOption> {
}
else {
this.restoreData();
- this.mergeOption(baseOption);
+ this._mergeOption(baseOption, opt);
}
optionChanged = true;
}
@@ -146,7 +180,7 @@ class GlobalModel extends Model<ECUnitOption> {
const timelineOption = optionManager.getTimelineOption(this);
if (timelineOption) {
optionChanged = true;
- this.mergeOption(timelineOption);
+ this._mergeOption(timelineOption, opt);
}
}
@@ -155,7 +189,7 @@ class GlobalModel extends Model<ECUnitOption> {
if (mediaOptions.length) {
each(mediaOptions, function (mediaOption) {
optionChanged = true;
- this.mergeOption(mediaOption);
+ this._mergeOption(mediaOption, opt);
}, this);
}
}
@@ -163,10 +197,19 @@ class GlobalModel extends Model<ECUnitOption> {
return optionChanged;
}
- mergeOption(newOption: ECUnitOption): void {
+ public mergeOption(option: ECUnitOption): void {
+ this._mergeOption(option, null);
+ }
+
+ private _mergeOption(
+ newOption: ECUnitOption,
+ opt: InnerSetOptionOpts
+ ): void {
const option = this.option;
const componentsMap = this._componentsMap;
- const newCptTypes: ComponentMainType[] = [];
+ const componentsCount = this._componentsCount;
+ const newCmptTypes: ComponentMainType[] = [];
+ const replaceMergeMainTypeMap = opt && opt.replaceMergeMainTypeMap;
resetSourceDefaulter(this);
@@ -184,12 +227,12 @@ class GlobalModel extends Model<ECUnitOption> {
: merge(option[mainType], componentOption, true);
}
else if (mainType) {
- newCptTypes.push(mainType);
+ newCmptTypes.push(mainType);
}
});
(ComponentModel as ComponentModelConstructor).topologicalTravel(
- newCptTypes,
+ newCmptTypes,
(ComponentModel as ComponentModelConstructor).getAllClassMainTypes(),
visitComponent,
this
@@ -201,41 +244,48 @@ class GlobalModel extends Model<ECUnitOption> {
dependencies: string | string[]
): void {
- const newCptOptionList = modelUtil.normalizeToArray(newOption[mainType]);
+ const newCmptOptionList = modelUtil.normalizeToArray(newOption[mainType]);
- const mapResult = modelUtil.mappingToExists(
- componentsMap.get(mainType), newCptOptionList
- );
+ const oldCmptList = componentsMap.get(mainType);
+ const mapResult = replaceMergeMainTypeMap && replaceMergeMainTypeMap.get(mainType)
+ ? modelUtil.mappingToExistsInReplaceMerge(oldCmptList, newCmptOptionList)
+ : modelUtil.mappingToExistsInNormalMerge(oldCmptList, newCmptOptionList);
modelUtil.makeIdAndName(mapResult);
// Set mainType and complete subType.
each(mapResult, function (item) {
- const opt = item.option;
+ const opt = item.newOption;
if (isObject(opt)) {
item.keyInfo.mainType = mainType;
- item.keyInfo.subType = determineSubType(mainType, opt, item.exist);
+ item.keyInfo.subType = determineSubType(mainType, opt, item.existing);
}
});
- option[mainType] = [];
- componentsMap.set(mainType, []);
+ // Set it before the travel, in case that `this._componentsMap` is
+ // used in some `init` or `merge` of components.
+ option[mainType] = null;
+ componentsMap.set(mainType, null);
+ componentsCount.set(mainType, 0);
+ const optionsByMainType = [] as ComponentOption[];
+ const cmptsByMainType = [] as ComponentModel[];
+ let cmptsCountByMainType = 0;
each(mapResult, function (resultItem, index) {
- let componentModel = resultItem.exist;
- const newCptOption = resultItem.option;
-
- assert(
- isObject(newCptOption) || componentModel,
- 'Empty component definition'
- );
-
- // Consider where is no new option and should be merged using {},
- // see removeEdgeAndAdd in topologicalTravel and
- // ComponentModel.getAllClassMainTypes.
- if (!newCptOption) {
- componentModel.mergeOption({}, this);
- componentModel.optionUpdated({}, false);
+ let componentModel = resultItem.existing;
+ const newCmptOption = resultItem.newOption;
+
+ if (!newCmptOption) {
+ if (componentModel) {
+ // Consider where is no new option and should be merged using {},
+ // see removeEdgeAndAdd in topologicalTravel and
+ // ComponentModel.getAllClassMainTypes.
+ componentModel.mergeOption({}, this);
+ componentModel.optionUpdated({}, false);
+ }
+ // If no both `resultItem.exist` and `resultItem.option`,
+ // either it is in `replaceMerge` and not matched by any id,
+ // or it has been removed in previous `replaceMerge` and left a "hole" in this component index.
}
else {
const ComponentModelClass = (ComponentModel as ComponentModelConstructor).getClass(
@@ -245,8 +295,8 @@ class GlobalModel extends Model<ECUnitOption> {
if (componentModel && componentModel.constructor === ComponentModelClass) {
componentModel.name = resultItem.keyInfo.name;
// componentModel.settingTask && componentModel.settingTask.dirty();
- componentModel.mergeOption(newCptOption, this);
- componentModel.optionUpdated(newCptOption, false);
+ componentModel.mergeOption(newCmptOption, this);
+ componentModel.optionUpdated(newCmptOption, false);
}
else {
// PENDING Global as parent ?
@@ -257,32 +307,48 @@ class GlobalModel extends Model<ECUnitOption> {
resultItem.keyInfo
);
componentModel = new ComponentModelClass(
- newCptOption, this, this, extraOpt
+ newCmptOption, this, this, extraOpt
);
extend(componentModel, extraOpt);
- componentModel.init(newCptOption, this, this);
+ if (resultItem.brandNew) {
+ componentModel.__requireNewView = true;
+ }
+ componentModel.init(newCmptOption, this, this);
// Call optionUpdated after init.
- // newCptOption has been used as componentModel.option
+ // newCmptOption has been used as componentModel.option
// and may be merged with theme and default, so pass null
// to avoid confusion.
componentModel.optionUpdated(null, true);
}
}
- componentsMap.get(mainType)[index] = componentModel;
- option[mainType][index] = componentModel.option;
+ if (componentModel) {
+ optionsByMainType.push(componentModel.option);
+ cmptsByMainType.push(componentModel);
+ cmptsCountByMainType++;
+ }
+ else {
+ // Always do assign to avoid elided item in array.
+ optionsByMainType.push(void 0);
+ cmptsByMainType.push(void 0);
+ }
}, this);
+ option[mainType] = optionsByMainType;
+ componentsMap.set(mainType, cmptsByMainType);
+ componentsCount.set(mainType, cmptsCountByMainType);
+
// Backup series for filtering.
if (mainType === 'series') {
- createSeriesIndices(this, componentsMap.get('series'));
+ reCreateSeriesIndices(this);
}
}
- this._seriesIndicesMap = createHashMap<number>(
- this._seriesIndices = this._seriesIndices || []
- );
+ // If no series declared, ensure `_seriesIndices` initialized.
+ if (!this._seriesIndices) {
+ reCreateSeriesIndices(this);
+ }
}
/**
@@ -294,12 +360,22 @@ class GlobalModel extends Model<ECUnitOption> {
each(option, function (opts, mainType) {
if ((ComponentModel as ComponentModelConstructor).hasClass(mainType)) {
opts = modelUtil.normalizeToArray(opts);
- for (let i = opts.length - 1; i >= 0; i--) {
+ // Inner cmpts need to be removed.
+ // Inner cmpts might not be at last since ec5.0, but still
+ // compatible for users: if inner cmpt at last, splice the returned array.
+ let realLen = opts.length;
+ let metNonInner = false;
+ for (let i = realLen - 1; i >= 0; i--) {
// Remove options with inner id.
- if (modelUtil.isIdInner(opts[i])) {
- opts.splice(i, 1);
+ if (opts[i] && !modelUtil.isIdInner(opts[i])) {
+ metNonInner = true;
+ }
+ else {
+ opts[i] = null;
+ !metNonInner && realLen--;
}
}
+ opts.length = realLen;
option[mainType] = opts;
}
});
@@ -323,51 +399,41 @@ class GlobalModel extends Model<ECUnitOption> {
}
}
+ /**
+ * @return Never be null/undefined.
+ */
queryComponents(condition: QueryConditionKindB): ComponentModel[] {
const mainType = condition.mainType;
if (!mainType) {
return [];
}
- let index = condition.index;
+ const index = condition.index;
const id = condition.id;
const name = condition.name;
+ const cmpts = this._componentsMap.get(mainType);
- const cpts = this._componentsMap.get(mainType);
-
- if (!cpts || !cpts.length) {
+ if (!cmpts || !cmpts.length) {
return [];
}
- let result;
+ let result: ComponentModel[];
if (index != null) {
- if (!isArray(index)) {
- index = [index];
- }
- result = filter(map(index, function (idx) {
- return cpts[idx];
- }), function (val) {
- return !!val;
+ result = [];
+ each(modelUtil.normalizeToArray(index), function (idx) {
+ cmpts[idx] && result.push(cmpts[idx]);
});
}
else if (id != null) {
- const isIdArray = isArray(id);
- result = filter(cpts, function (cpt) {
- return (isIdArray && indexOf(id as string[], cpt.id) >= 0)
- || (!isIdArray && cpt.id === id);
- });
+ result = queryByIdOrName('id', id, cmpts);
}
else if (name != null) {
- const isNameArray = isArray(name);
- result = filter(cpts, function (cpt) {
- return (isNameArray && indexOf(name as string[], cpt.name) >= 0)
- || (!isNameArray && cpt.name === name);
- });
+ result = queryByIdOrName('name', name, cmpts);
}
else {
- // Return all components with mainType
- result = cpts.slice();
+ // Return all non-empty components in that mainType
+ result = filter(cmpts, cmpt => !!cmpt);
}
return filterBySubType(result, condition);
@@ -397,7 +463,8 @@ class GlobalModel extends Model<ECUnitOption> {
const queryCond = getQueryCond(query);
const result = queryCond
? this.queryComponents(queryCond)
- : this._componentsMap.get(mainType);
+ // Retrieve all non-empty components.
+ : filter(this._componentsMap.get(mainType), cmpt => !!cmpt);
return doFilter(filterBySubType(result, condition));
@@ -428,6 +495,8 @@ class GlobalModel extends Model<ECUnitOption> {
}
/**
+ * Travel components (before filtered).
+ *
* @usage
* eachComponent('legend', function (legendModel, index) {
* ...
@@ -466,31 +535,44 @@ class GlobalModel extends Model<ECUnitOption> {
) {
const componentsMap = this._componentsMap;
- if (typeof mainType === 'function') {
- const contextReal = cb as T;
- const cbReal = mainType as EachComponentAllCallback;
- componentsMap.each(function (components, componentType) {
- each(components, function (component, index) {
- cbReal.call(contextReal, componentType, component, index);
- });
+ if (isFunction(mainType)) {
+ const ctxForAll = cb as T;
+ const cbForAll = mainType as EachComponentAllCallback;
+ componentsMap.each(function (cmpts, componentType) {
+ for (let i = 0; cmpts && i < cmpts.length; i++) {
+ const cmpt = cmpts[i];
+ cmpt && cbForAll.call(ctxForAll, componentType, cmpt, cmpt.componentIndex);
+ }
});
}
- else if (isString(mainType)) {
- each(componentsMap.get(mainType), cb as EachComponentInMainTypeCallback, context);
- }
- else if (isObject(mainType)) {
- const queryResult = this.findComponents(mainType);
- each(queryResult, cb as EachComponentInMainTypeCallback, context);
+ else {
+ const cmpts = isString(mainType)
+ ? componentsMap.get(mainType)
+ : isObject(mainType)
+ ? this.findComponents(mainType)
+ : null;
+ for (let i = 0; cmpts && i < cmpts.length; i++) {
+ const cmpt = cmpts[i];
+ cmpt && (cb as EachComponentInMainTypeCallback).call(
+ context, cmpt, cmpt.componentIndex
+ );
+ }
}
}
+ /**
+ * Get series list before filtered by name.
+ */
getSeriesByName(name: string): SeriesModel[] {
- const series = this._componentsMap.get('series') as SeriesModel[];
- return filter(series, function (oneSeries) {
- return oneSeries.name === name;
- });
+ return filter(
+ this._componentsMap.get('series') as SeriesModel[],
+ oneSeries => !!oneSeries && oneSeries.name === name
+ );
}
+ /**
+ * Get series list before filtered by index.
+ */
getSeriesByIndex(seriesIndex: number): SeriesModel {
return this._componentsMap.get('series')[seriesIndex] as SeriesModel;
}
@@ -500,18 +582,27 @@ class GlobalModel extends Model<ECUnitOption> {
* FIXME: rename to getRawSeriesByType?
*/
getSeriesByType(subType: ComponentSubType): SeriesModel[] {
- const series = this._componentsMap.get('series') as SeriesModel[];
- return filter(series, function (oneSeries) {
- return oneSeries.subType === subType;
- });
+ return filter(
+ this._componentsMap.get('series') as SeriesModel[],
+ oneSeries => !!oneSeries && oneSeries.subType === subType
+ );
}
+ /**
+ * Get all series before filtered.
+ */
getSeries(): SeriesModel[] {
- return this._componentsMap.get('series').slice() as SeriesModel[];
+ return filter(
+ this._componentsMap.get('series').slice() as SeriesModel[],
+ oneSeries => !!oneSeries
+ );
}
+ /**
+ * Count series before filtered.
+ */
getSeriesCount(): number {
- return this._componentsMap.get('series').length;
+ return this._componentsCount.get('series');
}
/**
@@ -539,7 +630,9 @@ class GlobalModel extends Model<ECUnitOption> {
cb: (this: T, series: SeriesModel, rawSeriesIndex: number) => void,
context?: T
): void {
- each(this._componentsMap.get('series'), cb, context);
+ each(this._componentsMap.get('series'), function (series) {
+ series && cb.call(context, series, series.componentIndex);
+ });
}
/**
@@ -573,7 +666,7 @@ class GlobalModel extends Model<ECUnitOption> {
isSeriesFiltered(seriesModel: SeriesModel): boolean {
assertSeriesInitialized(this);
- return this._seriesIndicesMap.get(seriesModel.componentIndex + '') == null;
+ return this._seriesIndicesMap.get(seriesModel.componentIndex) == null;
}
getCurrentSeriesIndices(): number[] {
@@ -585,17 +678,22 @@ class GlobalModel extends Model<ECUnitOption> {
context?: T
): void {
assertSeriesInitialized(this);
- const filteredSeries = filter(
- this._componentsMap.get('series') as SeriesModel[], cb, context
- );
- createSeriesIndices(this, filteredSeries);
+
+ const newSeriesIndices: number[] = [];
+ each(this._seriesIndices, function (seriesRawIdx) {
+ const series = this._componentsMap.get('series')[seriesRawIdx] as SeriesModel;
+ cb.call(context, series, seriesRawIdx) && newSeriesIndices.push(seriesRawIdx);
+ }, this);
+
+ this._seriesIndices = newSeriesIndices;
+ this._seriesIndicesMap = createHashMap(newSeriesIndices);
}
restoreData(payload?: Payload): void {
- const componentsMap = this._componentsMap;
- createSeriesIndices(this, componentsMap.get('series'));
+ reCreateSeriesIndices(this);
+ const componentsMap = this._componentsMap;
const componentTypes: string[] = [];
componentsMap.each(function (components, componentType) {
componentTypes.push(componentType);
@@ -604,10 +702,16 @@ class GlobalModel extends Model<ECUnitOption> {
(ComponentModel as ComponentModelConstructor).topologicalTravel(
componentTypes,
(ComponentModel as ComponentModelConstructor).getAllClassMainTypes(),
- function (componentType, dependencies) {
+ function (componentType) {
each(componentsMap.get(componentType), function (component) {
- (componentType !== 'series' || !isNotTargetSeries(component as SeriesModel, payload))
- && component.restoreData();
+ if (component
+ && (
+ componentType !== 'series'
+ || !isNotTargetSeries(component as SeriesModel, payload)
+ )
+ ) {
+ component.restoreData();
+ }
});
}
);
@@ -615,12 +719,13 @@ class GlobalModel extends Model<ECUnitOption> {
private static internalField = (function () {
- createSeriesIndices = function (ecModel: GlobalModel, seriesModels: ComponentModel[]): void {
- ecModel._seriesIndicesMap = createHashMap(
- ecModel._seriesIndices = map(seriesModels, function (series) {
- return series.componentIndex;
- }) || []
- );
+ reCreateSeriesIndices = function (ecModel: GlobalModel): void {
+ const seriesIndices: number[] = ecModel._seriesIndices = [];
+ each(ecModel._componentsMap.get('series'), function (series) {
+ // series may have been removed by `replaceMerge`.
+ series && seriesIndices.push(series.componentIndex);
+ });
+ ecModel._seriesIndicesMap = createHashMap(seriesIndices);
};
assertSeriesInitialized = function (ecModel: GlobalModel): void {
@@ -644,13 +749,14 @@ class GlobalModel extends Model<ECUnitOption> {
// Init with series: [], in case of calling findSeries method
// before series initialized.
ecModel._componentsMap = createHashMap({series: []});
+ ecModel._componentsCount = createHashMap();
mergeTheme(baseOption, ecModel._theme.option);
// TODO Needs clone when merging to the unexisted property
merge(baseOption, globalDefault, false);
- ecModel.mergeOption(baseOption);
+ ecModel._mergeOption(baseOption, null);
};
})();
@@ -692,10 +798,10 @@ export interface QueryConditionKindB {
name?: string | string[];
}
export interface EachComponentAllCallback {
- (mainType: string, model: ComponentModel, index: number): void;
+ (mainType: string, model: ComponentModel, componentIndex: number): void;
}
interface EachComponentInMainTypeCallback {
- (model: ComponentModel, index: number): void;
+ (model: ComponentModel, componentIndex: number): void;
}
@@ -719,7 +825,8 @@ function mergeTheme(option: ECUnitOption, theme: ThemeOption): void {
if (name === 'colorLayer' && notMergeColorLayer) {
return;
}
- // 如果有 component model 则把具体的 merge 逻辑交给该 model 处理
+ // If it is component model mainType, the model handles that merge later.
+ // otherwise, merge them here.
if (!(ComponentModel as ComponentModelConstructor).hasClass(name)) {
if (typeof themeItem === 'object') {
option[name] = !option[name]
@@ -737,20 +844,34 @@ function mergeTheme(option: ECUnitOption, theme: ThemeOption): void {
function determineSubType(
mainType: ComponentMainType,
- newCptOption: ComponentOption,
+ newCmptOption: ComponentOption,
existComponent: {subType: ComponentSubType} | ComponentModel
): ComponentSubType {
- const subType = newCptOption.type
- ? newCptOption.type
+ const subType = newCmptOption.type
+ ? newCmptOption.type
: existComponent
? existComponent.subType
// Use determineSubType only when there is no existComponent.
- : (ComponentModel as ComponentModelConstructor).determineSubType(mainType, newCptOption);
+ : (ComponentModel as ComponentModelConstructor).determineSubType(mainType, newCmptOption);
// tooltip, markline, markpoint may always has no subType
return subType;
}
+function queryByIdOrName<T extends { id?: string, name?: string }>(
+ attr: 'id' | 'name',
+ idOrName: string | string[],
+ cmpts: T[]
+): T[] {
+ let keyMap: HashMap<string>;
+ return isArray(idOrName)
+ ? (
+ keyMap = createHashMap(idOrName),
+ filter(cmpts, cmpt => cmpt && keyMap.get(cmpt[attr]) != null)
+ )
+ : filter(cmpts, cmpt => cmpt && cmpt[attr] === idOrName + '');
+}
+
function filterBySubType(
components: ComponentModel[],
condition: QueryConditionKindA | QueryConditionKindB
@@ -758,14 +879,27 @@ function filterBySubType(
// Using hasOwnProperty for restrict. Consider
// subType is undefined in user payload.
return condition.hasOwnProperty('subType')
- ? filter(components, function (cpt) {
- return cpt.subType === condition.subType;
- })
+ ? filter(components, cmpt => cmpt && cmpt.subType === condition.subType)
: components;
}
-// @ts-ignore FIXME:GlobalOption
-interface GlobalModel extends ColorPaletteMixin {}
+function normalizeReplaceMergeInput(opts: GlobalModelSetOptionOpts): InnerSetOptionOpts {
+ const replaceMergeMainTypeMap = createHashMap<boolean>();
+ opts && each(modelUtil.normalizeToArray(opts.replaceMerge), function (mainType) {
+ if (__DEV__) {
+ assert(
+ (ComponentModel as ComponentModelConstructor).hasClass(mainType),
+ '"' + mainType + '" is not valid component main type in "replaceMerge"'
+ );
+ }
+ replaceMergeMainTypeMap.set(mainType, true);
+ });
+ return {
+ replaceMergeMainTypeMap: replaceMergeMainTypeMap
+ };
+}
+
+interface GlobalModel extends ColorPaletteMixin<ECUnitOption> {}
mixin(GlobalModel, ColorPaletteMixin);
export default GlobalModel;
diff --git a/src/model/OptionManager.ts b/src/model/OptionManager.ts
index e5b123f..ee195e8 100644
--- a/src/model/OptionManager.ts
+++ b/src/model/OptionManager.ts
@@ -27,7 +27,7 @@ import * as modelUtil from '../util/model';
import ComponentModel, { ComponentModelConstructor } from './Component';
import ExtensionAPI from '../ExtensionAPI';
import { OptionPreprocessor, MediaQuery, ECUnitOption, MediaUnit, ECOption } from '../util/types';
-import GlobalModel from './Global';
+import GlobalModel, { InnerSetOptionOpts } from './Global';
const each = zrUtil.each;
const clone = zrUtil.clone;
@@ -80,7 +80,11 @@ class OptionManager {
this._api = api;
}
- setOption(rawOption: ECOption, optionPreprocessorFuncs: OptionPreprocessor[]): void {
+ setOption(
+ rawOption: ECOption,
+ optionPreprocessorFuncs: OptionPreprocessor[],
+ opt: InnerSetOptionOpts
+ ): void {
if (rawOption) {
// That set dat primitive is dangerous if user reuse the data when setOption again.
zrUtil.each(modelUtil.normalizeToArray((rawOption as ECUnitOption).series), function (series) {
@@ -106,7 +110,7 @@ class OptionManager {
// For setOption at second time (using merge mode);
if (oldOptionBackup) {
// Only baseOption can be merged.
- mergeOption(oldOptionBackup.baseOption, newParsedOption.baseOption);
+ mergeOption(oldOptionBackup.baseOption, newParsedOption.baseOption, opt);
// For simplicity, timeline options and media options do not support merge,
// that is, if you `setOption` twice and both has timeline options, the latter
@@ -184,7 +188,8 @@ class OptionManager {
}
// FIXME
- // 是否mediaDefault应该强制用户设置,否则可能修改不能回归。
+ // Whether mediaDefault should force users to provide? Otherwise
+ // the change by media query can not be recorvered.
if (!indices.length && mediaDefault) {
indices = [-1];
}
@@ -345,8 +350,18 @@ function indicesEquals(indices1: number[], indices2: number[]): boolean {
* 1. Each model handle its self restoration but not uniform treatment.
* (Too complex in logic and error-prone)
* 2. Use a shadow ecModel. (Performace expensive)
+ *
+ * FIXME: A possible solution:
+ * Add a extra level of model for each component model. The inheritance chain would be:
+ * ecModel <- componentModel <- componentActionModel <- dataItemModel
+ * And all of the actions can only modify the `componentActionModel` rather than
+ * `componentModel`. `setOption` will only modify the `ecModel` and `componentModel`.
+ * When "resotre" action triggered, model from `componentActionModel` will be discarded
+ * instead of recreating the "ecModel" from the "_optionBackup".
*/
-function mergeOption(oldOption: ECUnitOption, newOption: ECUnitOption): void {
+function mergeOption(
+ oldOption: ECUnitOption, newOption: ECUnitOption, opt: InnerSetOptionOpts
+): void {
newOption = newOption || {} as ECUnitOption;
each(newOption, function (newCptOpt, mainType) {
@@ -363,12 +378,14 @@ function mergeOption(oldOption: ECUnitOption, newOption: ECUnitOption): void {
newCptOpt = modelUtil.normalizeToArray(newCptOpt);
oldCptOpt = modelUtil.normalizeToArray(oldCptOpt);
- const mapResult = modelUtil.mappingToExists(oldCptOpt, newCptOpt);
+ const mapResult = opt.replaceMergeMainTypeMap.get(mainType)
+ ? modelUtil.mappingToExistsInReplaceMerge(oldCptOpt, newCptOpt)
+ : modelUtil.mappingToExistsInNormalMerge(oldCptOpt, newCptOpt);
oldOption[mainType] = map(mapResult, function (item) {
- return (item.option && item.exist)
- ? merge(item.exist, item.option, true)
- : (item.exist || item.option);
+ return (item.newOption && item.existing)
+ ? merge(item.existing, item.newOption, true)
+ : (item.existing || item.newOption);
});
}
});
diff --git a/src/util/model.ts b/src/util/model.ts
index 1589c80..95cb000 100644
--- a/src/util/model.ts
+++ b/src/util/model.ts
@@ -46,6 +46,7 @@ import { Dictionary } from 'zrender/src/core/types';
import SeriesModel from '../model/Series';
import CartesianAxisModel from '../coord/cartesian/AxisModel';
import GridModel from '../coord/cartesian/GridModel';
+import { __DEV__ } from '../config';
/**
* Make the name displayable. But we should
@@ -142,11 +143,24 @@ export function isDataItemOption(dataItem: OptionDataItem): boolean {
}
type MappingExistItem = {id?: string, name?: string} | ComponentModel;
+/**
+ * The array `MappingResult<T>[]` exactly represents the content of the result
+ * components array after merge.
+ * The indices are the same as the `existings`.
+ * Items will not be `null`/`undefined` even if the corresponding `existings` will be removed.
+ */
+type MappingResult<T> = MappingResultItem<T>[];
interface MappingResultItem<T> {
- exist?: T;
- option?: ComponentOption;
- id?: string;
- name?: string;
+ // Existing component instance.
+ existing?: T;
+ // The mapped new component option.
+ newOption?: ComponentOption;
+ // Mark that the new component has nothing to do with any of the old components.
+ // So they won't share view. Also see `__requireNewView`.
+ brandNew?: boolean;
+ // id?: string;
+ // name?: string;
+ // keyInfo for new component option.
keyInfo?: {
name?: string,
id?: string,
@@ -156,106 +170,192 @@ interface MappingResultItem<T> {
}
/**
- * Mapping to exists for merge.
+ * Mapping to existings for merge.
+ * The mapping result (merge result) will keep the order of the existing
+ * component, rather than the order of new option. Because we should ensure
+ * some specified index reference (like xAxisIndex) keep work.
+ * And in most cases, "merge option" is used to update partial option but not
+ * be expected to change the order.
*
- * @public
- * @param exists
- * @param newCptOptions
- * @return Result, like [{exist: ..., option: ...}, {}],
- * index of which is the same as exists.
+ * @return See the comment of <MappingResult>.
*/
-export function mappingToExists<T extends MappingExistItem>(
- exists: T[],
- newCptOptions: ComponentOption[]
-): MappingResultItem<T>[] {
- // Mapping by the order by original option (but not order of
- // new option) in merge mode. Because we should ensure
- // some specified index (like xAxisIndex) is consistent with
- // original option, which is easy to understand, espatially in
- // media query. And in most case, merge option is used to
- // update partial option but not be expected to change order.
- newCptOptions = (newCptOptions || []).slice();
-
- const result: MappingResultItem<T>[] = map(exists || [], function (obj, index) {
- return {exist: obj};
- });
+export function mappingToExistsInNormalMerge<T extends MappingExistItem>(
+ existings: T[],
+ newCmptOptions: ComponentOption[]
+): MappingResult<T> {
+ newCmptOptions = (newCmptOptions || []).slice();
+ existings = existings || [];
+
+ const result: MappingResultItem<T>[] = [];
+ // Do not use native `map` to in case that the array `existings`
+ // contains elided items, which will be ommited.
+ for (let index = 0; index < existings.length; index++) {
+ // Because of replaceMerge, `existing` may be null/undefined.
+ result.push({ existing: existings[index] });
+ }
// Mapping by id or name if specified.
- each(newCptOptions, function (cptOption, index) {
- if (!isObject<ComponentOption>(cptOption)) {
+ each(newCmptOptions, function (cmptOption, index) {
+ if (!isObject<ComponentOption>(cmptOption)) {
+ newCmptOptions[index] = null;
return;
}
// id has highest priority.
for (let i = 0; i < result.length; i++) {
- if (!result[i].option // Consider name: two map to one.
- && cptOption.id != null
- && result[i].exist.id === cptOption.id + ''
+ const existing = result[i].existing;
+ if (!result[i].newOption // Consider name: two map to one.
+ && cmptOption.id != null
+ && existing
+ && existing.id === cmptOption.id + ''
) {
- result[i].option = cptOption;
- newCptOptions[index] = null;
+ result[i].newOption = cmptOption;
+ newCmptOptions[index] = null;
return;
}
}
for (let i = 0; i < result.length; i++) {
- const exist = result[i].exist;
- if (!result[i].option // Consider name: two map to one.
- // Can not match when both ids exist but different.
- && (exist.id == null || cptOption.id == null)
- && cptOption.name != null
- && !isIdInner(cptOption)
- && !isIdInner(exist)
- && exist.name === cptOption.name + ''
+ const existing = result[i].existing;
+ if (!result[i].newOption // Consider name: two map to one.
+ // Can not match when both ids existing but different.
+ && existing
+ && (existing.id == null || cmptOption.id == null)
+ && cmptOption.name != null
+ && !isIdInner(cmptOption)
+ && !isIdInner(existing)
+ && existing.name === cmptOption.name + ''
) {
- result[i].option = cptOption;
- newCptOptions[index] = null;
+ result[i].newOption = cmptOption;
+ newCmptOptions[index] = null;
return;
}
}
});
- // Otherwise mapping by index.
- each(newCptOptions, function (cptOption, index) {
- if (!isObject<ComponentOption>(cptOption)) {
- return;
- }
+ mappingByIndexFinally(newCmptOptions, result, false);
- let i = 0;
- for (; i < result.length; i++) {
- const exist = result[i].exist;
- if (!result[i].option
- // Existing model that already has id should be able to
- // mapped to (because after mapping performed model may
- // be assigned with a id, whish should not affect next
- // mapping), except those has inner id.
- && !isIdInner(exist)
- // Caution:
- // Do not overwrite id. But name can be overwritten,
- // because axis use name as 'show label text'.
- // 'exist' always has id and name and we dont
- // need to check it.
- && cptOption.id == null
- ) {
- result[i].option = cptOption;
- break;
+ return result;
+}
+
+/**
+ * Mapping to exists for merge.
+ * The mode "replaceMerge" means that:
+ * (1) Only the id mapped components will be merged.
+ * (2) Other existing components (except inner compoonets) will be removed.
+ * (3) Other new options will be used to create new component.
+ * (4) The index of the existing compoents will not be modified.
+ * That means their might be "hole" after the removal.
+ * The new components are created first at those available index.
+ *
+ * @return See the comment of <MappingResult>.
+ */
+export function mappingToExistsInReplaceMerge<T extends MappingExistItem>(
+ existings: T[],
+ newCmptOptions: ComponentOption[]
+): MappingResult<T> {
+
+ existings = existings || [];
+ newCmptOptions = (newCmptOptions || []).slice();
+ const existingIdIdxMap = createHashMap<number>();
+ const result = [] as MappingResult<T>;
+
+ // Do not use native `each` to in case that the array `existings`
+ // contains elided items, which will be ommited.
+ for (let index = 0; index < existings.length; index++) {
+ const existing = existings[index];
+ let innerExisting: T;
+ // Because of replaceMerge, `existing` may be null/undefined.
+ if (existing) {
+ if (isIdInner(existing)) {
+ // inner components should not be removed.
+ innerExisting = existing;
}
+ // Input with inner id is allowed for convenience of some internal usage.
+ existingIdIdxMap.set(existing.id, index);
}
+ result.push({ existing: innerExisting });
+ }
- if (i >= result.length) {
- result.push({option: cptOption});
+ // Mapping by id if specified.
+ each(newCmptOptions, function (cmptOption, index) {
+ if (!isObject<ComponentOption>(cmptOption)) {
+ newCmptOptions[index] = null;
+ return;
+ }
+ const optionId = cmptOption.id + '';
+ const existingIdx = existingIdIdxMap.get(optionId);
+ if (existingIdx != null) {
+ if (__DEV__) {
+ assert(
+ !result[existingIdx].newOption,
+ 'Duplicated option on id "' + optionId + '".'
+ );
+ }
+ result[existingIdx].newOption = cmptOption;
+ // Mark not to be removed but to be merged.
+ // In this case the existing component will be merged with the new option if `subType` is the same,
+ // or replaced with a new created component if the `subType` is different.
+ result[existingIdx].existing = existings[existingIdx];
+ newCmptOptions[index] = null;
}
});
+ mappingByIndexFinally(newCmptOptions, result, true);
+
+ // The array `result` MUST NOT contain elided items, otherwise the
+ // forEach will ommit those items and result in incorrect result.
return result;
}
+function mappingByIndexFinally<T extends MappingExistItem>(
+ newCmptOptions: ComponentOption[],
+ mappingResult: MappingResult<T>,
+ allBrandNew: boolean
+): void {
+ let nextIdx = 0;
+ each(newCmptOptions, function (cmptOption) {
+ if (!cmptOption) {
+ return;
+ }
+
+ // Find the first place that not mapped by id and not inner component (consider the "hole").
+ let resultItem;
+ while (
+ // Be `!resultItem` only when `nextIdx >= mappingResult.length`.
+ (resultItem = mappingResult[nextIdx])
+ // (1) Existing models that already have id should be able to mapped to. Because
+ // after mapping performed, model will always be assigned with an id if user not given.
+ // After that all models have id.
+ // (2) If new option has id, it can only set to a hole or append to the last. It should
+ // not be merged to the existings with different id. Because id should not be overwritten.
+ // (3) Name can be overwritten, because axis use name as 'show label text'.
+ && (
+ (cmptOption.id != null && resultItem.existing)
+ || resultItem.newOption
+ || isIdInner(resultItem.existing)
+ )
+ ) {
+ nextIdx++;
+ }
+
+ if (resultItem) {
+ resultItem.newOption = cmptOption;
+ resultItem.brandNew = allBrandNew;
+ }
+ else {
+ mappingResult.push({ newOption: cmptOption, brandNew: allBrandNew });
+ }
+ nextIdx++;
+ });
+}
+
/**
* Make id and name for mapping result (result of mappingToExists)
* into `keyInfo` field.
*/
export function makeIdAndName(
- mapResult: MappingResultItem<MappingExistItem>[]
+ mapResult: MappingResult<MappingExistItem>
): void {
// We use this id to hash component models and view instances
// in echarts. id can be specified by user, or auto generated.
@@ -271,13 +371,13 @@ export function makeIdAndName(
// Ensure that each id is distinct.
const idMap = createHashMap();
- each(mapResult, function (item, index) {
- const existCpt = item.exist;
- existCpt && idMap.set(existCpt.id, item);
+ each(mapResult, function (item) {
+ const existing = item.existing;
+ existing && idMap.set(existing.id, item);
});
- each(mapResult, function (item, index) {
- const opt = item.option;
+ each(mapResult, function (item) {
+ const opt = item.newOption;
assert(
!opt || opt.id == null || !idMap.get(opt.id) || idMap.get(opt.id) === item,
@@ -290,8 +390,8 @@ export function makeIdAndName(
// Make name and id.
each(mapResult, function (item, index) {
- const existCpt = item.exist;
- const opt = item.option;
+ const existing = item.existing;
+ const opt = item.newOption;
const keyInfo = item.keyInfo;
if (!isObject<ComponentOption>(opt)) {
@@ -304,14 +404,14 @@ export function makeIdAndName(
// instance will be recreated, which can be accepted.
keyInfo.name = opt.name != null
? opt.name + ''
- : existCpt
- ? existCpt.name
+ : existing
+ ? existing.name
// Avoid diffferent series has the same name,
// because name may be used like in color pallet.
: DUMMY_COMPONENT_NAME_PREFIX + index;
- if (existCpt) {
- keyInfo.id = existCpt.id;
+ if (existing) {
+ keyInfo.id = existing.id;
}
else if (opt.id != null) {
keyInfo.id = opt.id + '';
@@ -341,13 +441,13 @@ export function isNameSpecified(componentModel: ComponentModel): boolean {
/**
* @public
- * @param {Object} cptOption
+ * @param {Object} cmptOption
* @return {boolean}
*/
-export function isIdInner(cptOption: ComponentOption): boolean {
- return isObject(cptOption)
- && cptOption.id
- && (cptOption.id + '').indexOf('\0_ec_\0') === 0;
+export function isIdInner(cmptOption: ComponentOption): boolean {
+ return cmptOption
+ && cmptOption.id
+ && (cmptOption.id + '').indexOf('\0_ec_\0') === 0;
}
type BatchItem = {
diff --git a/src/util/types.ts b/src/util/types.ts
index 50b7f7c..4ab0b19 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -352,7 +352,7 @@ export type ECUnitOption = {
media?: never
timeline?: ComponentOption | ComponentOption[]
[key: string]: ComponentOption | ComponentOption[] | Dictionary<any> | any
-} & AnimationOptionMixin;
+} & AnimationOptionMixin & ColorPaletteOptionMixin;
/**
* [ECOption]:
diff --git a/test/lib/testHelper.js b/test/lib/testHelper.js
index 5e6c6fe..f5a5f0f 100644
--- a/test/lib/testHelper.js
+++ b/test/lib/testHelper.js
@@ -261,6 +261,64 @@
}
};
+ /**
+ * @usage
+ * ```js
+ * testHelper.printAssert(chart, function (assert) {
+ * // If any error thrown here, a "checked: Fail" will be printed on the chart;
+ * // Otherwise, "checked: Pass" will be printed on the chart.
+ * assert(condition1);
+ * assert(condition2);
+ * assert(condition3);
+ * });
+ * ```
+ * `testHelper.printAssert` can be called multiple times for one chart instance.
+ * For each call, one result (fail or pass) will be printed.
+ *
+ * @param chart {EChartsInstance}
+ * @param checkFn {Function} param: a function `assert`.
+ */
+ testHelper.printAssert = function (chart, checkerFn) {
+ var failErr;
+ function assert(cond) {
+ if (!cond) {
+ throw new Error();
+ }
+ }
+ try {
+ checkerFn(assert);
+ }
+ catch (err) {
+ console.error(err);
+ failErr = err;
+ }
+ var printAssertRecord = chart.__printAssertRecord || (chart.__printAssertRecord = []);
+
+ var resultDom = document.createElement('div');
+ resultDom.innerHTML = failErr ? 'checked: Fail' : 'checked: Pass';
+ var fontSize = 40;
+ resultDom.style.cssText = [
+ 'position: absolute;',
+ 'left: 20px;',
+ 'font-size: ' + fontSize + 'px;',
+ 'z-index: ' + (failErr ? 99999 : 88888) + ';',
+ 'color: ' + (failErr ? 'red' : 'green') + ';',
+ ].join('');
+ printAssertRecord.push(resultDom);
+ chart.getDom().appendChild(resultDom);
+
+ relayoutResult();
+
+ function relayoutResult() {
+ var chartHeight = chart.getHeight();
+ var lineHeight = Math.min(fontSize + 10, (chartHeight - 20) / printAssertRecord.length);
+ for (var i = 0; i < printAssertRecord.length; i++) {
+ var record = printAssertRecord[i];
+ record.style.top = (10 + i * lineHeight) + 'px';
+ }
+ }
+ };
+
var _dummyRequestAnimationFrameMounted = false;
diff --git a/test/option-replaceMerge.html b/test/option-replaceMerge.html
new file mode 100644
index 0000000..2513ec2
--- /dev/null
+++ b/test/option-replaceMerge.html
@@ -0,0 +1,661 @@
+<!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/facePrint.js"></script>
+ <script src="lib/testHelper.js"></script>
+ <!-- <script src="ut/lib/canteen.js"></script> -->
+ <link rel="stylesheet" href="lib/reset.css" />
+ </head>
+ <body>
+ <style>
+ </style>
+
+
+
+ <div id="main_normalMerge_basic"></div>
+ <div id="main_replaceMerge_basic"></div>
+ <div id="main_normalMerge_add"></div>
+ <div id="main_replaceMerge_add_no_id"></div>
+ <div id="main_replaceMerge_add_new_id"></div>
+ <div id="main_replaceMerge_add_find_hole"></div>
+ <div id="main_normalMerge_add_find_hole"></div>
+ <div id="main_replaceMerge_inner_and_other_cmpt_not_effect"></div>
+ <div id="main_replaceMerge_remove_all"></div>
+
+
+
+
+ <script>
+ function makeBasicOption(opt) {
+ return {
+ xAxis: {
+ type: 'category'
+ },
+ yAxis: {},
+ legend: {},
+ tooltip: {},
+ dataZoom: [{
+ type: 'slider'
+ }, {
+ type: 'inside'
+ }],
+ series: [{
+ id: 'a',
+ name: 'aa',
+ type: 'line',
+ data: [['a11', 22], ['a33', 44]]
+ }, {
+ id: 'b',
+ name: 'bb',
+ type: 'line',
+ data: [['a11', 55], ['a33', 77]]
+ }, {
+ id: 'c',
+ name: 'cc',
+ type: 'line',
+ data: [['a11', 66], ['a33', 100]]
+ }, {
+ id: 'd',
+ name: 'dd',
+ type: 'line',
+ data: [['a11', 99], ['a33', 130]]
+ }, {
+ name: 'no_id',
+ type: 'line',
+ data: [['a11', 130], ['a33', 160]]
+ }]
+ };
+ }
+
+ </script>
+
+
+
+ <!-- ----------------------------- -->
+ <!-- ----------------------------- -->
+ <!-- ----------------------------- -->
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ var option = makeBasicOption();
+
+ var chart = testHelper.create(echarts, 'main_normalMerge_basic', {
+ title: [
+ 'normalMerge: basic case',
+ 'click "setOption": "bb" become bar chart, "aa" become **rect** symbol',
+ 'other series **do not change**'
+ ],
+ option: option,
+ buttons: [{
+ text: 'setOption',
+ onclick: function () {
+ chart.setOption({
+ series: [{
+ id: 'b',
+ type: 'bar',
+ data: [['a11', 55], ['a33', 77]]
+ }, {
+ id: 'a',
+ symbol: 'rect'
+ }]
+ })
+ }
+ }],
+ height: 300
+ });
+ });
+ </script>
+
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ var option = makeBasicOption();
+
+ var chart = testHelper.create(echarts, 'main_replaceMerge_basic', {
+ title: [
+ 'replaceMerge: basic case',
+ 'click "setOption": "bb" become bar chart, "aa" become **rect** symbol',
+ 'other series **removed**'
+ ],
+ option: option,
+ buttons: [{
+ text: 'setOption',
+ onclick: function () {
+ chart.setOption({
+ series: [{
+ id: 'b',
+ type: 'bar',
+ data: [['a11', 55], ['a33', 77]]
+ }, {
+ id: 'a',
+ symbol: 'rect'
+ }]
+ }, {replaceMerge: 'series'})
+ }
+ }, {
+ text: 'check after click setOption',
+ onclick: function () {
+ testHelper.printAssert(chart, function (assert) {
+ var seriesModels = chart.getModel().getSeries();
+ assert(seriesModels.length === 2);
+ assert(chart.getModel().getSeriesCount() === 2);
+ });
+ }
+ }],
+ height: 300
+ });
+ });
+ </script>
+
+
+
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ var option = makeBasicOption();
+
+ var chart = testHelper.create(echarts, 'main_normalMerge_add', {
+ title: [
+ 'normalMerge: add',
+ 'click "setOption": "aa" become **rect** symbol, "no_id" become "new_no_id" and bar',
+ 'other series **do not change**'
+ ],
+ option: option,
+ buttons: [{
+ text: 'setOption',
+ onclick: function () {
+ chart.setOption({
+ series: [{
+ id: 'a',
+ symbol: 'rect'
+ }, {
+ name: 'new_no_id',
+ type: 'bar',
+ data: [['a11', 10], ['a33', 20]]
+ }]
+ })
+ }
+ }],
+ height: 300
+ });
+ });
+ </script>
+
+
+
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ var option = makeBasicOption();
+
+ var chart = testHelper.create(echarts, 'main_replaceMerge_add_no_id', {
+ title: [
+ 'replaceMerge: add (add no id)',
+ 'click "setOption": "aa" become **rect** symbol, bar series "new_no_id" added',
+ 'other series **removed**',
+ 'click "check": should show **checked: Pass**'
+ ],
+ option: option,
+ buttons: [{
+ text: 'setOption',
+ onclick: function () {
+ chart.setOption({
+ series: [{
+ id: 'a',
+ symbol: 'rect'
+ }, {
+ name: 'new_no_id',
+ type: 'bar',
+ data: [['a11', 10], ['a33', 20]]
+ }]
+ }, {replaceMerge: ['series']});
+ }
+ }, {
+ text: 'check after click setOption',
+ onclick: function () {
+ testHelper.printAssert(chart, function (assert) {
+ var seriesModels = chart.getModel().getSeries();
+ assert(seriesModels.length === 2);
+ assert(
+ seriesModels[1].componentIndex === 1
+ && seriesModels[1].name === 'new_no_id'
+ );
+ assert(chart.getModel().getSeriesCount() === 2);
+ });
+ }
+ }],
+ height: 300
+ });
+ });
+ </script>
+
+
+
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ var option = makeBasicOption();
+
+ var chart = testHelper.create(echarts, 'main_replaceMerge_add_new_id', {
+ title: [
+ 'replaceMerge: add (has new id)',
+ 'click "setOption": "aa" become **rect** symbol, bar series "xx" added',
+ 'other series **removed**',
+ 'click "check": should show **checked: Pass**'
+ ],
+ option: option,
+ buttons: [{
+ text: 'setOption',
+ onclick: function () {
+ chart.setOption({
+ series: [{
+ id: 'a',
+ symbol: 'rect'
+ }, {
+ id: 'x',
+ name: 'xx',
+ type: 'bar',
+ data: [['a11', 10], ['a33', 20]]
+ }]
+ }, {replaceMerge: 'series'});
+ }
+ }, {
+ text: 'check after click setOption',
+ onclick: function () {
+ testHelper.printAssert(chart, function (assert) {
+ var seriesModels = chart.getModel().getSeries();
+ assert(seriesModels.length === 2);
+ assert(
+ seriesModels[1].componentIndex === 1
+ && seriesModels[1].name === 'xx'
+ );
+ assert(chart.getModel().getSeriesCount() === 2);
+ });
+ }
+ }],
+ height: 300
+ });
+ });
+ </script>
+
+
+
+
+
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ var option = makeBasicOption();
+
+ var chart = testHelper.create(echarts, 'main_replaceMerge_add_find_hole', {
+ title: [
+ '**replaceMerge**: add (find the first hole)',
+ 'click the buttons one by one from left to right',
+ 'should show **TWO checked: Pass**'
+ ],
+ option: option,
+ buttons: [{
+ text: 'setOption_remove',
+ onclick: function () {
+ chart.setOption({
+ series: [{
+ id: 'a'
+ }, {
+ id: 'c'
+ }, {
+ id: 'd'
+ }]
+ }, {replaceMerge: 'series'});
+ }
+ }, {
+ text: 'check after click setOption_remove',
+ onclick: function () {
+ testHelper.printAssert(chart, function (assert) {
+ var seriesModels = chart.getModel().getSeries();
+ assert(seriesModels.length === 3);
+ assert(seriesModels[0].componentIndex === 0);
+ assert(seriesModels[1].componentIndex === 2);
+ assert(seriesModels[2].componentIndex === 3);
+ assert(seriesModels[0].id === 'a');
+ assert(seriesModels[1].id === 'c');
+ assert(seriesModels[2].id === 'd');
+
+ assert(chart.getModel().getSeriesCount() === 3);
+
+ var optionGotten = chart.getOption();
+ assert(optionGotten.series.length === 4);
+ assert(optionGotten.series[0].name === 'aa');
+ assert(optionGotten.series[1] == null);
+ assert(optionGotten.series[2].name === 'cc');
+ assert(optionGotten.series[3].name === 'dd');
+
+ assert(chart.getModel().getSeriesByIndex(1) == null);
+ assert(chart.getModel().getComponent('series', 1) == null);
+ });
+ }
+ }, {
+ text: 'setOption_replaceMerge',
+ onclick: function () {
+ chart.setOption({
+ series: [{
+ id: 'm',
+ type: 'bar',
+ data: [['a11', 22], ['a33', 44]]
+ }, {
+ id: 'n',
+ type: 'bar',
+ data: [['a11', 32], ['a33', 54]]
+ }, {
+ id: 'a'
+ }, {
+ id: 'c'
+ }, {
+ id: 'd'
+ }]
+ }, {replaceMerge: 'series'});
+ }
+ }, {
+ text: 'check after click setOption_replaceMerge',
+ onclick: function () {
+ testHelper.printAssert(chart, function (assert) {
+ var seriesModels = chart.getModel().getSeries();
+ assert(seriesModels.length === 5);
+ assert(seriesModels[0].componentIndex === 0);
+ assert(seriesModels[1].componentIndex === 1);
+ assert(seriesModels[2].componentIndex === 2);
+ assert(seriesModels[3].componentIndex === 3);
+ assert(seriesModels[4].componentIndex === 4);
+ assert(seriesModels[0].id === 'a');
+ assert(seriesModels[1].id === 'm');
+ assert(seriesModels[2].id === 'c');
+ assert(seriesModels[3].id === 'd');
+ assert(seriesModels[4].id === 'n');
+
+ assert(chart.getModel().getSeriesCount() === 5);
+
+ var optionGotten = chart.getOption();
+ assert(optionGotten.series.length === 5);
+ assert(optionGotten.series[0].id === 'a');
+ assert(optionGotten.series[1].id === 'm');
+ assert(optionGotten.series[2].id === 'c');
+ assert(optionGotten.series[3].id === 'd');
+ assert(optionGotten.series[4].id === 'n');
+
+ assert(chart.getModel().getSeriesByIndex(1).id == 'm');
+ assert(chart.getModel().getComponent('series', 1).id == 'm');
+ });
+ }
+ }],
+ height: 300
+ });
+ });
+ </script>
+
+
+
+
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ var option = makeBasicOption();
+
+ var chart = testHelper.create(echarts, 'main_normalMerge_add_find_hole', {
+ title: [
+ '**normalMerge**: add (find the first hole)',
+ 'click the buttons one by one from left to right',
+ 'should show **TWO checked: Pass**'
+ ],
+ option: option,
+ buttons: [{
+ text: 'setOption_remove',
+ onclick: function () {
+ chart.setOption({
+ series: [{
+ id: 'a'
+ }, {
+ id: 'c'
+ }, {
+ id: 'd'
+ }]
+ }, {replaceMerge: 'series'});
+ }
+ }, {
+ text: 'check after click setOption_remove',
+ onclick: function () {
+ testHelper.printAssert(chart, function (assert) {
+ var seriesModels = chart.getModel().getSeries();
+ assert(seriesModels.length === 3);
+ assert(seriesModels[0].componentIndex === 0);
+ assert(seriesModels[1].componentIndex === 2);
+ assert(seriesModels[2].componentIndex === 3);
+ assert(seriesModels[0].id === 'a');
+ assert(seriesModels[1].id === 'c');
+ assert(seriesModels[2].id === 'd');
+
+ assert(chart.getModel().getSeriesCount() === 3);
+
+ var optionGotten = chart.getOption();
+ assert(optionGotten.series.length === 4);
+ assert(optionGotten.series[0].name === 'aa');
+ assert(optionGotten.series[1] == null);
+ assert(optionGotten.series[2].name === 'cc');
+ assert(optionGotten.series[3].name === 'dd');
+
+ assert(chart.getModel().getSeriesByIndex(1) == null);
+ assert(chart.getModel().getComponent('series', 1) == null);
+ });
+ }
+ }, {
+ text: 'setOption_normalMerge',
+ onclick: function () {
+ chart.setOption({
+ series: [{
+ id: 'm',
+ type: 'bar',
+ data: [['a11', 22], ['a33', 44]]
+ }, {
+ id: 'n',
+ type: 'bar',
+ data: [['a11', 32], ['a33', 54]]
+ }]
+ });
+ }
+ }, {
+ text: 'check after click setOption_normalMerge',
+ onclick: function () {
+ testHelper.printAssert(chart, function (assert) {
+ var seriesModels = chart.getModel().getSeries();
+ assert(seriesModels.length === 5);
+ assert(seriesModels[0].componentIndex === 0);
+ assert(seriesModels[1].componentIndex === 1);
+ assert(seriesModels[2].componentIndex === 2);
+ assert(seriesModels[3].componentIndex === 3);
+ assert(seriesModels[4].componentIndex === 4);
+ assert(seriesModels[0].id === 'a');
+ assert(seriesModels[1].id === 'm');
+ assert(seriesModels[2].id === 'c');
+ assert(seriesModels[3].id === 'd');
+ assert(seriesModels[4].id === 'n');
+
+ assert(chart.getModel().getSeriesCount() === 5);
+
+ var optionGotten = chart.getOption();
+ assert(optionGotten.series.length === 5);
+ assert(optionGotten.series[0].id === 'a');
+ assert(optionGotten.series[1].id === 'm');
+ assert(optionGotten.series[2].id === 'c');
+ assert(optionGotten.series[3].id === 'd');
+ assert(optionGotten.series[4].id === 'n');
+
+ assert(chart.getModel().getSeriesByIndex(1).id == 'm');
+ assert(chart.getModel().getComponent('series', 1).id == 'm');
+ });
+ }
+ }],
+ height: 300
+ });
+ });
+ </script>
+
+
+
+
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ var option = {
+ xAxis: {
+ type: 'category'
+ },
+ yAxis: {},
+ legend: {},
+ tooltip: {},
+ toolbox: {
+ feature: {
+ dataZoom: {}
+ }
+ },
+ dataZoom: [{
+ id: 'inside_dz',
+ type: 'inside'
+ }],
+ series: [{
+ id: 'a',
+ name: 'aa',
+ type: 'line',
+ data: [['a11', 22], ['a33', 44]]
+ }, {
+ id: 'b',
+ name: 'bb',
+ type: 'line',
+ data: [['a11', 55], ['a33', 77]]
+ }]
+ };
+
+ var chart = testHelper.create(echarts, 'main_replaceMerge_inner_and_other_cmpt_not_effect', {
+ title: [
+ 'replaceMerge: inner not effect',
+ 'click "setOption": a dataZoom.slider added',
+ 'check **inside dataZoom** and **select dataZoom** on toolbox still OK',
+ 'series **not change**',
+ 'click "check": should show **checked: Pass**'
+ ],
+ option: option,
+ buttons: [{
+ text: 'setOption',
+ onclick: function () {
+ chart.setOption({
+ dataZoom: [{
+ type: 'slider'
+ }, {
+ id: 'inside_dz'
+ }]
+ }, {replaceMerge: ['dataZoom']});
+ }
+ }, {
+ text: 'check after click setOption',
+ onclick: function () {
+ testHelper.printAssert(chart, function (assert) {
+ var insideDZ = chart.getModel().getComponent('dataZoom', 0);
+ var selectDZX = chart.getModel().getComponent('dataZoom', 1);
+ var selectDZY = chart.getModel().getComponent('dataZoom', 2);
+ var sliderDZ = chart.getModel().getComponent('dataZoom', 3);
+ assert(insideDZ.type === 'dataZoom.inside');
+ assert(selectDZX.type === 'dataZoom.select');
+ assert(selectDZY.type === 'dataZoom.select');
+ assert(sliderDZ.type === 'dataZoom.slider');
+ assert(chart.getModel().getComponent('dataZoom', 4) == null);
+ });
+ }
+ }],
+ height: 300
+ });
+ });
+ </script>
+
+
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ var option = makeBasicOption();
+
+ var chart = testHelper.create(echarts, 'main_replaceMerge_remove_all', {
+ title: [
+ 'replaceMerge: remove all',
+ 'click "setOption": "all series should be removed"',
+ 'click "check": should show **checked: Pass**'
+ ],
+ option: option,
+ buttons: [{
+ text: 'setOption',
+ onclick: function () {
+ chart.setOption({
+ series: []
+ }, {replaceMerge: 'series'});
+ }
+ }, {
+ text: 'check after click setOption',
+ onclick: function () {
+ testHelper.printAssert(chart, function (assert) {
+ var seriesModels = chart.getModel().getSeries();
+ assert(seriesModels.length === 0);
+ assert(chart.getModel().getSeriesCount() === 0);
+ });
+ }
+ }],
+ height: 300
+ });
+ });
+ </script>
+
+
+
+
+
+
+ </body>
+</html>
+
diff --git a/test/option-replaceMerge2.html b/test/option-replaceMerge2.html
new file mode 100644
index 0000000..cd85164
--- /dev/null
+++ b/test/option-replaceMerge2.html
@@ -0,0 +1,497 @@
+<!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/facePrint.js"></script>
+ <script src="lib/testHelper.js"></script>
+ <!-- <script src="ut/lib/canteen.js"></script> -->
+ <link rel="stylesheet" href="lib/reset.css" />
+ </head>
+ <body>
+ <style>
+ </style>
+
+
+
+ <div id="transition_facet_cartesian"></div>
+ <div id="notMerge_transition_replaceMerge_newView"></div>
+ <div id="main_replaceMerge_keep_update"></div>
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ var optionBase = {
+ color: ['#eb6134', '#eb9934', '#348feb', '#36b6d9'],
+ dataset: {
+ source: [
+ [null, 'sweet zongzi', 'salty zongzi', 'sweet milk tea', 'salty milk tea'],
+ ['2012-01', 32, 65, 71, 31],
+ ['2012-02', 41, 67, 89, 23],
+ ['2012-03', 58, 61, 97, 12],
+ ['2012-04', 67, 73, 105, 9],
+ ['2012-05', 72, 67, 122, 18],
+ ['2012-06', 94, 79, 118, 32],
+ ['2012-07', 79, 89, 131, 37],
+ ['2012-08', 65, 76, 103, 41],
+ ['2012-09', 69, 81, 84, 48],
+ ['2012-10', 74, 64, 104, 38],
+ ['2012-11', 91, 76, 111, 51],
+ ['2012-12', 64, 68, 121, 61]
+ ]
+ },
+ legend: {},
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ dataZoom: [{
+ type: 'slider',
+ height: 15
+ }, {
+ type: 'inside'
+ }],
+ series: [{
+ type: 'bar',
+ encode: { x: 0, y: 1, seriesName: 1 }
+ }, {
+ type: 'bar',
+ encode: { x: 0, y: 3, seriesName: 3 }
+ }, {
+ type: 'bar',
+ encode: { x: 0, y: 2, seriesName: 2 }
+ }, {
+ type: 'bar',
+ encode: { x: 0, y: 4, seriesName: 4 }
+ }]
+ };
+
+ var optionSingle = makeSingleCartesianOption();
+ var option0 = mergeOption(echarts, optionBase, optionSingle);
+
+ function mergeOption(echarts, target, source) {
+ echarts.util.each(source, function (srcCmpts, mainType) {
+ var tarCmpts = target[mainType] = toArray(target[mainType]);
+ echarts.util.each(toArray(srcCmpts), function (srcCmpt, index) {
+ tarCmpts[index] = echarts.util.merge(tarCmpts[index], srcCmpt, true);
+ });
+ });
+ function toArray(some) {
+ return echarts.util.isArray(some) ? some : some ? [some] : [];
+ }
+ return target;
+ }
+
+ function makeSingleCartesianOption() {
+ return {
+ grid: {
+ },
+ xAxis: {
+ type: 'category'
+ },
+ yAxis: {
+ max: 150,
+ axisLine: { show: false },
+ axisTick: { show: false }
+ },
+ axisPointer: {
+ link: [{xAxisIndex: 0}]
+ },
+ dataZoom: [{
+ xAxisIndex: 0
+ }, {
+ xAxisIndex: 0
+ }],
+ series: [{
+ xAxisIndex: 0,
+ yAxisIndex: 0
+ }, {
+ xAxisIndex: 0,
+ yAxisIndex: 0
+ }, {
+ xAxisIndex: 0,
+ yAxisIndex: 0
+ }, {
+ xAxisIndex: 0,
+ yAxisIndex: 0
+ }]
+ };
+ }
+ function makeDoubleCartesianOption() {
+ return {
+ grid: [{
+ bottom: '52%'
+ }, {
+ top: '52%'
+ }],
+ dataZoom: [{
+ xAxisIndex: [0, 1]
+ }, {
+ xAxisIndex: [0, 1]
+ }],
+ axisPointer: {
+ link: [{xAxisIndex: [0, 1]}]
+ },
+ xAxis: [{
+ type: 'category',
+ axisLabel: { show: false },
+ axisTick: { show: false },
+ axisLine: { show: false },
+ gridIndex: 0
+ }, {
+ type: 'category',
+ // axisTick: { show: false },
+ axisLine: { onZero: false },
+ gridIndex: 1
+ }],
+ yAxis: [{
+ name: 'sweet',
+ max: 150,
+ nameLocation: 'center',
+ nameGap: 40,
+ axisLine: { show: false },
+ axisTick: { show: false },
+ axisLabel: { color: '#000' },
+ gridIndex: 0
+ }, {
+ name: 'salty',
+ max: 150,
+ nameLocation: 'center',
+ nameGap: 40,
+ inverse: true,
+ axisLine: { show: false },
+ axisTick: { show: false },
+ axisLabel: { color: '#000' },
+ gridIndex: 1
+ }],
+ series: [{
+ xAxisIndex: 0,
+ yAxisIndex: 0
+ }, {
+ xAxisIndex: 0,
+ yAxisIndex: 0
+ }, {
+ xAxisIndex: 1,
+ yAxisIndex: 1
+ }, {
+ xAxisIndex: 1,
+ yAxisIndex: 1
+ }]
+ };
+ }
+
+ var chart = testHelper.create(echarts, 'transition_facet_cartesian', {
+ title: [
+ '<1> Click "double cartesian", should become **double** grid',
+ 'Click "single cartesian", should become **single** grid',
+ 'Check transition animation existing',
+ '<2> **downplay some legend item**, then click "doulbe"/"single" btns again',
+ 'transition should be OK, legend state should be kept',
+ '<3> **shrink dataZoom**, then click "doulbe"/"single" btns again',
+ 'transition should be OK, legend state should be kept',
+ ],
+ option: option0,
+ buttons: [{
+ text: 'double cartesian',
+ onclick: function () {
+ chart.setOption(makeDoubleCartesianOption(), {
+ replaceMerge: ['xAxis', 'yAxis', 'grid']
+ })
+ }
+ }, {
+ text: 'single cartesian',
+ onclick: function () {
+ chart.setOption(makeSingleCartesianOption(), {
+ replaceMerge: ['xAxis', 'yAxis', 'grid']
+ })
+ }
+ }]
+ });
+ });
+ </script>
+
+
+
+
+
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ function makeOption(extOption) {
+ return {
+ animationDurationUpdate: 2000,
+ dataset: {
+ source: [
+ [null, 'sweet zongzi', 'salty zongzi', 'sweet milk tea', 'salty milk tea', 'NewNew'],
+ ['2012-01', 32, 65, 71, 31, 99],
+ ['2012-02', 41, 67, 99, 23, 199],
+ ['2012-03', 58, 61, 97, 12, 99],
+ ['2012-04', 67, 73, 105, 9, 199],
+ ['2012-05', 72, 67, 122, 18, 99],
+ ['2012-06', 94, 79, 118, 32, 199],
+ ]
+ },
+ legend: {},
+ tooltip: {
+ },
+ xAxis: {
+ type: 'category'
+ },
+ yAxis: {},
+ series: extOption.series
+ };
+ }
+
+ var option_base = makeOption({
+ series: [{
+ type: 'scatter',
+ encode: { x: 0, y: 1, seriesName: 1 }
+ }, {
+ type: 'scatter',
+ encode: { x: 0, y: 3, seriesName: 3 }
+ }]
+ });
+
+ var option_notMerge = makeOption({
+ series: [{
+ type: 'scatter',
+ encode: { x: 0, y: 2, seriesName: 2 }
+ }, {
+ type: 'scatter',
+ encode: { x: 0, y: 4, seriesName: 4 }
+ }]
+ });
+
+ var option_replaceMerge = {
+ series: [{
+ type: 'scatter',
+ encode: { x: 0, y: 5, seriesName: 5 }
+ }]
+ };
+
+ var seriesModels_base;
+ var seriesModels_notMerge;
+ var seriesModels_replaceMerge;
+ var view0_notMerge;
+ var view0_replaceMerge;
+
+ var chart = testHelper.create(echarts, 'notMerge_transition_replaceMerge_newView', {
+ title: [
+ 'Click btns from left to right:',
+ 'Click "setOption_notMerge", should **has trans animation**',
+ 'Click "check", should print **checked: Pass**',
+ 'Click "setOption_replaceMerge", should only "NewNew" and **no trans animation**',
+ 'Click "check", should print **checked: Pass**',
+ ],
+ option: option_base,
+ buttons: [{
+ text: 'setOption_notMerge',
+ onclick: function () {
+ seriesModels_base = chart.getModel().getSeries('series');
+ chart.setOption(option_notMerge, true);
+ }
+ }, {
+ text: 'then check',
+ onclick: function () {
+ testHelper.printAssert(chart, function (assert) {
+ seriesModels_notMerge = chart.getModel().getSeries();
+ assert(seriesModels_base.length === 2);
+ assert(seriesModels_notMerge.length === 2);
+
+ assert(seriesModels_base[0] !== seriesModels_notMerge[0]);
+ assert(seriesModels_base[1] !== seriesModels_notMerge[1]);
+ assert(seriesModels_base[0] !== seriesModels_notMerge[1]);
+ assert(seriesModels_base[1] !== seriesModels_notMerge[0]);
+ });
+ }
+ }, {
+ text: 'setOption_replaceMerge',
+ onclick: function () {
+ seriesModels_base = chart.getModel().getSeries('series');
+ view0_notMerge = chart.getViewOfSeriesModel(seriesModels_notMerge[0]);
+ chart.setOption(option_replaceMerge, { replaceMerge: 'series' });
+ }
+ }, {
+ text: 'then check',
+ onclick: function () {
+ testHelper.printAssert(chart, function (assert) {
+ seriesModels_replaceMerge = chart.getModel().getSeries();
+ assert(seriesModels_replaceMerge.length === 1);
+ assert(seriesModels_notMerge[0] !== seriesModels_replaceMerge[0]);
+ view0_replaceMerge = chart.getViewOfSeriesModel(seriesModels_replaceMerge[0]);
+ assert(view0_notMerge != null);
+ assert(view0_notMerge !== view0_replaceMerge);
+ });
+ }
+ }]
+ });
+ });
+ </script>
+
+
+
+
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ var currentRound = 0;
+ var nameMap = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n'];
+ var categories = ['Mon', 'Tue', 'Wed'];
+
+ function createUpdatableSeriesAndDataset(seriesCount) {
+ var series = [];
+ for (var i = 0; i < seriesCount; i++) {
+ series.push({
+ name: nameMap[i] + '_round_' + currentRound,
+ type: 'bar',
+ barWidth: 40,
+ encode: {
+ x: 0,
+ y: i + 1
+ },
+ seriesLayoutBy: 'row'
+ });
+ }
+ var dataset = {
+ source: [categories.slice()]
+ };
+ var yVal = 22 + 100 * currentRound;
+ for (var i = 0; i < seriesCount; i++, yVal += 10) {
+ dataset.source.push([yVal, yVal * 2, yVal * 2.5]);
+ }
+
+ currentRound++;
+
+ return {
+ series: series,
+ dataset: dataset
+ };
+ }
+
+ var sInfo = createUpdatableSeriesAndDataset(2);
+ var series = sInfo.series;
+ var dataset = sInfo.dataset;
+
+ series.unshift({
+ id: 'I_never_change',
+ name: 'I_never_change',
+ type: 'pie',
+ selectedMode: 'single',
+ lineStyle: {
+ color: '#881100',
+ width: 5
+ },
+ center: ['20%', 80],
+ radius: 40,
+ data: [
+ {name: 'Mon', value: 100},
+ {name: 'Tue', value: 200},
+ {name: 'Wed', value: 150}
+ ]
+ });
+
+ var option = {
+ dataset: dataset,
+ brush: {
+ toolbox: ['polygon', 'rect', 'lineX', 'lineY', 'keep', 'clear'],
+ xAxisIndex: 'all',
+ },
+ xAxis: {
+ type: 'category'
+ },
+ yAxis: {},
+ legend: {},
+ tooltip: {},
+ dataZoom: [{
+ type: 'slider'
+ }, {
+ type: 'inside'
+ }],
+ series: series
+ };
+
+ var chart = testHelper.create(echarts, 'main_replaceMerge_keep_update', {
+ title: [
+ 'replaceMerge: keep update',
+ '<1> click "replace to new 4 series": bar totally replaced to new 4 different bars',
+ 'click "replace to new 2 series": bar totally replaced to new 2 different bars',
+ 'series "I_never_change" **never change color and data**',
+ 'click "check": should show **checked: Pass**',
+ '<2> click pie legend to hide a sector',
+ 'click pie to select a sector',
+ 'click buttons again, **pie state should not changed**',
+ '<3> use brush',
+ 'click buttons again, **brush selected should be correct**',
+ ],
+ option: option,
+ height: 400,
+ buttons: [{
+ text: 'replace to new 4 series',
+ onclick: function () {
+ var sInfo = createUpdatableSeriesAndDataset(4);
+ sInfo.series.push({id: 'I_never_change'});
+ chart.setOption({
+ dataset: sInfo.dataset,
+ series: sInfo.series
+ }, {replaceMerge: ['series', 'dataset']});
+ }
+ }, {
+ text: 'replace to new 2 series',
+ onclick: function () {
+ var sInfo = createUpdatableSeriesAndDataset(2);
+ sInfo.series.push({id: 'I_never_change'});
+ chart.setOption({
+ dataset: sInfo.dataset,
+ series: sInfo.series
+ }, {replaceMerge: ['series', 'dataset']});
+ }
+ }, {
+ text: 'check after click setOption',
+ onclick: function () {
+ testHelper.printAssert(chart, function (assert) {
+ var seriesModels = chart.getModel().getSeries();
+ assert(seriesModels.length <= 6);
+ assert(chart.getModel().getSeriesCount() <= 6);
+ });
+ }
+ }]
+ });
+ });
+ </script>
+
+
+
+
+ </body>
+</html>
+
diff --git a/test/timeline-dynamic-series.html b/test/timeline-dynamic-series.html
index 1c786ac..7db7bdf 100644
--- a/test/timeline-dynamic-series.html
+++ b/test/timeline-dynamic-series.html
@@ -21,130 +21,109 @@ 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>
- <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <script src="lib/jquery.min.js"></script>
+ <script src="lib/facePrint.js"></script>
+ <script src="lib/testHelper.js"></script>
+ <link rel="stylesheet" href="lib/reset.css" />
</head>
<body>
<style>
- html, body, #main {
- width: 100%;
- height: 100%;
- margin: 0;
- }
- #main {
- background: #fff;
- }
</style>
- <div id="main"></div>
-
- <script>
-
-// markLine: {
-// symbol: ['arrow','none'],
-// symbolSize: [4, 2],
-// itemStyle: {
-// normal: {
-// lineStyle: {color:'orange'},
-// barBorderColor:'orange',
-// label: {
-// position:'left',
-// formatter:function(params){
-// return Math.round(params.value);
-// },
-// textStyle:{color:'orange'}
-// }
-// }
-// },
-// data: [{type: 'average', name: '平均值'}]
-// }
- require([
- 'echarts'
- // 'echarts/chart/bar',
- // 'echarts/chart/pie',
- // 'echarts/component/title',
- // 'echarts/component/legend',
- // 'echarts/component/grid',
- // 'echarts/component/tooltip',
- // 'echarts/component/timeline'
- ], function (echarts) {
+ <div id="main0"></div>
- var chart = echarts.init(document.getElementById('main'), null, {
+ <script>
- });
+ require(['echarts'], function (echarts) {
-var option = {
- baseOption: {
- timeline: {
- // y: 0,
- axisType: 'category',
- // realtime: false,
- // loop: false,
- autoPlay: false,
- // currentIndex: 2,
- playInterval: 1000,
- // controlStyle: {
- // position: 'left'
- // },
- data: [
- '2002-01-01','2003-01-01'
- ],
- label: {
- formatter : function(s) {
- return (new Date(s)).getFullYear();
- }
- }
- },
- title: {
- subtext: '数据来自国家统计局'
- },
- tooltip: {
- trigger:'axis',
- axisPointer: {
- type: 'shadow'
- }
- },
- calculable: true,
- grid: {
- top:80, bottom: 100
- },
- xAxis: {
- 'type':'category',
- 'axisLabel':{'interval':0},
- 'data':[
- '北京','\n天津','河北','\n山西'
- ],
- splitLine: {show: false}
- },
- yAxis: [
- {
- type: 'value',
- name: 'GDP(亿元)'
- }
- ],
- series: [
- ]
- },
- options: [
- {
- series: [
- {id: 'a', type: 'bar', data: [12, 33, 44, 55]}
- ]
- },
- {
- series : [
- {id: 'a', type: 'bar', data: [22, 33, 44, 55]},
- {id: 'b', type: 'bar', data: [55, 66, 77, 88]}
- ]
- }
- ]
-};
+ var option = {
+ baseOption: {
+ timeline: {
+ // y: 0,
+ axisType: 'category',
+ // realtime: false,
+ // loop: false,
+ autoPlay: false,
+ // currentIndex: 2,
+ playInterval: 1000,
+ // controlStyle: {
+ // position: 'left'
+ // },
+ replaceMerge: 'series',
+ data: [
+ '2 series', '3 series', '1 series'
+ ],
+ },
+ tooltip: {
+ trigger:'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ legend: {},
+ calculable: true,
+ grid: {
+ top:80, bottom: 100
+ },
+ toolbox: {
+ left: 'center',
+ top: 30,
+ feature: {
+ dataZoom: {}
+ }
+ },
+ xAxis: {
+ type: 'category',
+ data: ['CityB', 'CityT', 'CityH', 'CityS'],
+ splitLine: {show: false}
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: 'GDP'
+ }
+ ],
+ series: [
+ ]
+ },
+ options: [
+ {
+ series: [
+ {name: 'a', type: 'bar', data: [12, 33, 44, 55]},
+ {name: 'b', type: 'bar', data: [55, 66, 77, 88]},
+ ]
+ },
+ {
+ series : [
+ {name: 'a', type: 'bar', data: [22, 33, 44, 55]},
+ {name: 'b', type: 'bar', data: [55, 66, 77, 88]},
+ {name: 'c', type: 'bar', data: [55, 66, 77, 88]}
+ ]
+ },
+ {
+ series : [
+ {name: 'b', type: 'bar', data: [55, 66, 77, 88]}
+ ]
+ }
+ ]
+ };
- chart.setOption(option);
- window.onresize = chart.resize;
+ var chart = testHelper.create(echarts, 'main0', {
+ title: [
+ 'Click "right arrow" of the timeline',
+ 'check it should be **2 bar** => **3 bar** => **1 bar**',
+ 'use toolbox "区域缩放"',
+ 'timeline change',
+ 'use toolbox "区域缩放还原"',
+ 'Should be able to return correctly.'
+ ],
+ option: option,
});
+ });
</script>
</body>
</html>
\ No newline at end of file
diff --git a/test/timeline-life.html b/test/timeline-life.html
new file mode 100755
index 0000000..a9e8e4c
--- /dev/null
+++ b/test/timeline-life.html
@@ -0,0 +1,279 @@
+<!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/facePrint.js"></script>
+ <script src="lib/testHelper.js"></script>
+ <link rel="stylesheet" href="lib/reset.css">
+ </head>
+ <body>
+ <style>
+ .test-title {
+ background: #146402;
+ color: #fff;
+ }
+ #main0 {
+ height: 500px;
+ }
+ </style>
+
+
+ <div id="main0"></div>
+
+
+ <script>
+
+ var chart;
+ var myChart;
+ var option;
+
+ require(['echarts'], function (echarts) {
+
+ var myChart = echarts.init(document.getElementById('main0'));
+
+ $.get('data/life-expectancy.json', function (data) {
+ myChart.hideLoading();
+
+ var itemStyle = {
+ normal: {
+ opacity: 0.8,
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowOffsetY: 0,
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
+ }
+ };
+
+ var sizeFunction = function (x) {
+ var y = Math.sqrt(x / 5e8) + 0.1;
+ return y * 80;
+ };
+ // Schema:
+ var schema = [
+ {name: 'Income', index: 0, text: '人均收入', unit: '美元'},
+ {name: 'LifeExpectancy', index: 1, text: '人均寿命', unit: '岁'},
+ {name: 'Population', index: 2, text: '总人口', unit: ''},
+ {name: 'Country', index: 3, text: '国家', unit: ''}
+ ];
+
+ option = {
+ baseOption: {
+ timeline: {
+ axisType: 'category',
+ orient: 'vertical',
+ autoPlay: false,
+ inverse: true,
+ playInterval: 1000,
+ left: null,
+ right: 0,
+ top: 20,
+ bottom: 20,
+ width: 55,
+ height: null,
+ label: {
+ normal: {
+ textStyle: {
+ color: '#999'
+ }
+ },
+ emphasis: {
+ textStyle: {
+ color: '#fff'
+ }
+ }
+ },
+ symbol: 'none',
+ lineStyle: {
+ color: '#555'
+ },
+ checkpointStyle: {
+ color: '#bbb',
+ borderColor: '#777',
+ borderWidth: 2
+ },
+ controlStyle: {
+ showNextBtn: false,
+ showPrevBtn: false,
+ normal: {
+ color: '#666',
+ borderColor: '#666'
+ },
+ emphasis: {
+ color: '#aaa',
+ borderColor: '#aaa'
+ }
+ },
+ data: []
+ },
+ backgroundColor: '#404a59',
+ title: [{
+ text: data.timeline[0],
+ textAlign: 'center',
+ left: '63%',
+ top: '55%',
+ textStyle: {
+ fontSize: 100,
+ color: 'rgba(255, 255, 255, 0.7)'
+ }
+ }, {
+ text: '各国人均寿命与GDP关系演变',
+ left: 'center',
+ top: 10,
+ textStyle: {
+ color: '#aaa',
+ fontWeight: 'normal',
+ fontSize: 20
+ }
+ }],
+ tooltip: {
+ padding: 5,
+ backgroundColor: '#222',
+ borderColor: '#777',
+ borderWidth: 1,
+ formatter: function (obj) {
+ var value = obj.value;
+ return schema[3].text + ':' + value[3] + '<br>'
+ + schema[1].text + ':' + value[1] + schema[1].unit + '<br>'
+ + schema[0].text + ':' + value[0] + schema[0].unit + '<br>'
+ + schema[2].text + ':' + value[2] + '<br>';
+ }
+ },
+ grid: {
+ top: 100,
+ containLabel: true,
+ left: 30,
+ right: '110'
+ },
+ xAxis: {
+ type: 'log',
+ name: '人均收入',
+ max: 100000,
+ min: 300,
+ nameGap: 25,
+ nameLocation: 'middle',
+ nameTextStyle: {
+ fontSize: 18
+ },
+ splitLine: {
+ show: false
+ },
+ axisLine: {
+ lineStyle: {
+ color: '#ccc'
+ }
+ },
+ axisLabel: {
+ formatter: '{value} $'
+ }
+ },
+ yAxis: {
+ type: 'value',
+ name: '平均寿命',
+ max: 100,
+ nameTextStyle: {
+ color: '#ccc',
+ fontSize: 18
+ },
+ axisLine: {
+ lineStyle: {
+ color: '#ccc'
+ }
+ },
+ splitLine: {
+ show: false
+ },
+ axisLabel: {
+ formatter: '{value} 岁'
+ }
+ },
+ visualMap: [
+ {
+ show: false,
+ type: 'piecewise',
+ dimension: 3,
+ categories: echarts.util.map(data.countries, function (item) {
+ return item[2];
+ }),
+ calculable: true,
+ precision: 0.1,
+ textGap: 30,
+ textStyle: {
+ color: '#ccc'
+ },
+ inRange: {
+ color: (function () {
+ var colors = ['#bcd3bb', '#e88f70', '#edc1a5', '#9dc5c8', '#e1e8c8', '#7b7c68', '#e5b5b5', '#f0b489', '#928ea8', '#bda29a'];
+ return colors.concat(colors);
+ })()
+ }
+ }
+ ],
+ series: [
+ {
+ type: 'scatter',
+ itemStyle: itemStyle,
+ // progressive: false,
+ data: data.series[0],
+ symbolSize: function(val) {
+ return sizeFunction(val[2]);
+ }
+ }
+ ],
+ animationDurationUpdate: 1000,
+ animationEasingUpdate: 'quinticInOut'
+ },
+ options: []
+ };
+
+ for (var n = 0; n < data.timeline.length; n++) {
+ option.baseOption.timeline.data.push(data.timeline[n]);
+ option.options.push({
+ title: {
+ show: true,
+ 'text': data.timeline[n] + ''
+ },
+ series: {
+ name: data.timeline[n],
+ type: 'scatter',
+ itemStyle: itemStyle,
+ data: data.series[n],
+ symbolSize: function(val) {
+ return sizeFunction(val[2]);
+ }
+ }
+ });
+ }
+
+ myChart.setOption(option);
+
+ });
+
+ });
+
+ </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