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 <sushuang0...@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-unsubscr...@echarts.apache.org For additional commands, e-mail: commits-h...@echarts.apache.org