|  | /* | 
|  | * 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. | 
|  | */ | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Caution: If the mechanism should be changed some day, these cases | 
|  | * should be considered: | 
|  | * | 
|  | * (1) In `merge option` mode, if using the same option to call `setOption` | 
|  | * many times, the result should be the same (try our best to ensure that). | 
|  | * (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) 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 { | 
|  | 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'; | 
|  | import ComponentModel, {ComponentModelConstructor} from './Component'; | 
|  | import globalDefault from './globalDefault'; | 
|  | import {resetSourceDefaulter} from '../data/helper/sourceHelper'; | 
|  | import SeriesModel from './Series'; | 
|  | import { | 
|  | Payload, | 
|  | OptionPreprocessor, | 
|  | ECBasicOption, | 
|  | ECUnitOption, | 
|  | ThemeOption, | 
|  | ComponentOption, | 
|  | ComponentMainType, | 
|  | ComponentSubType, | 
|  | OptionId, | 
|  | OptionName, | 
|  | AriaOptionMixin | 
|  | } from '../util/types'; | 
|  | import OptionManager from './OptionManager'; | 
|  | import Scheduler from '../core/Scheduler'; | 
|  | import { concatInternalOptions } from './internalComponentCreator'; | 
|  | import { LocaleOption } from '../core/locale'; | 
|  | import {PaletteMixin} from './mixin/palette'; | 
|  | import { error, warn } from '../util/log'; | 
|  |  | 
|  | export interface GlobalModelSetOptionOpts { | 
|  | replaceMerge: ComponentMainType | ComponentMainType[]; | 
|  | } | 
|  | export interface InnerSetOptionOpts { | 
|  | replaceMergeMainTypeMap: HashMap<boolean, string>; | 
|  | } | 
|  |  | 
|  | // ----------------------- | 
|  | // Internal method names: | 
|  | // ----------------------- | 
|  | let reCreateSeriesIndices: (ecModel: GlobalModel) => void; | 
|  | let assertSeriesInitialized: (ecModel: GlobalModel) => void; | 
|  | let initBase: (ecModel: GlobalModel, baseOption: ECUnitOption) => void; | 
|  |  | 
|  | const OPTION_INNER_KEY = '\0_ec_inner'; | 
|  | const OPTION_INNER_VALUE = 1; | 
|  |  | 
|  | const BUITIN_COMPONENTS_MAP = { | 
|  | grid: 'GridComponent', | 
|  | polar: 'PolarComponent', | 
|  | geo: 'GeoComponent', | 
|  | singleAxis: 'SingleAxisComponent', | 
|  | parallel: 'ParallelComponent', | 
|  | calendar: 'CalendarComponent', | 
|  | graphic: 'GraphicComponent', | 
|  | toolbox: 'ToolboxComponent', | 
|  | tooltip: 'TooltipComponent', | 
|  | axisPointer: 'AxisPointerComponent', | 
|  | brush: 'BrushComponent', | 
|  | title: 'TitleComponent', | 
|  | timeline: 'TimelineComponent', | 
|  | markPoint: 'MarkPointComponent', | 
|  | markLine: 'MarkLineComponent', | 
|  | markArea: 'MarkAreaComponent', | 
|  | legend: 'LegendComponent', | 
|  | dataZoom: 'DataZoomComponent', | 
|  | visualMap: 'VisualMapComponent', | 
|  | // aria: 'AriaComponent', | 
|  | // dataset: 'DatasetComponent', | 
|  |  | 
|  | // Dependencies | 
|  | xAxis: 'GridComponent', | 
|  | yAxis: 'GridComponent', | 
|  | angleAxis: 'PolarComponent', | 
|  | radiusAxis: 'PolarComponent' | 
|  | } as const; | 
|  |  | 
|  | const BUILTIN_CHARTS_MAP = { | 
|  | line: 'LineChart', | 
|  | bar: 'BarChart', | 
|  | pie: 'PieChart', | 
|  | scatter: 'ScatterChart', | 
|  | radar: 'RadarChart', | 
|  | map: 'MapChart', | 
|  | tree: 'TreeChart', | 
|  | treemap: 'TreemapChart', | 
|  | graph: 'GraphChart', | 
|  | gauge: 'GaugeChart', | 
|  | funnel: 'FunnelChart', | 
|  | parallel: 'ParallelChart', | 
|  | sankey: 'SankeyChart', | 
|  | boxplot: 'BoxplotChart', | 
|  | candlestick: 'CandlestickChart', | 
|  | effectScatter: 'EffectScatterChart', | 
|  | lines: 'LinesChart', | 
|  | heatmap: 'HeatmapChart', | 
|  | pictorialBar: 'PictorialBarChart', | 
|  | themeRiver: 'ThemeRiverChart', | 
|  | sunburst: 'SunburstChart', | 
|  | custom: 'CustomChart' | 
|  | } as const; | 
|  |  | 
|  | const componetsMissingLogPrinted: Record<string, boolean> = {}; | 
|  |  | 
|  | function checkMissingComponents(option: ECUnitOption) { | 
|  | each(option, function (componentOption, mainType: ComponentMainType) { | 
|  | if (!ComponentModel.hasClass(mainType)) { | 
|  | const componentImportName = BUITIN_COMPONENTS_MAP[mainType as keyof typeof BUITIN_COMPONENTS_MAP]; | 
|  | if (componentImportName && !componetsMissingLogPrinted[componentImportName]) { | 
|  | error(`Component ${mainType} is used but not imported. | 
|  | import { ${componentImportName} } from 'echarts/components'; | 
|  | echarts.use([${componentImportName}]);`); | 
|  | componetsMissingLogPrinted[componentImportName] = true; | 
|  | } | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | class GlobalModel extends Model<ECUnitOption> { | 
|  | // @readonly | 
|  | option: ECUnitOption; | 
|  |  | 
|  | private _theme: Model; | 
|  |  | 
|  | private _locale: Model; | 
|  |  | 
|  | private _optionManager: OptionManager; | 
|  |  | 
|  | private _componentsMap: HashMap<ComponentModel[], ComponentMainType>; | 
|  |  | 
|  | /** | 
|  | * `_componentsMap` might have "hole" because 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. | 
|  | * Keep consistent with `_seriesIndices`. | 
|  | */ | 
|  | private _seriesIndicesMap: HashMap<any>; | 
|  |  | 
|  | /** | 
|  | * Model for store update payload | 
|  | */ | 
|  | private _payload: Payload; | 
|  |  | 
|  | // Injectable properties: | 
|  | scheduler: Scheduler; | 
|  |  | 
|  | // If in ssr mode. | 
|  | // TODO put in a better place? | 
|  | ssr: boolean; | 
|  |  | 
|  | init( | 
|  | option: ECBasicOption, | 
|  | parentModel: Model, | 
|  | ecModel: GlobalModel, | 
|  | theme: object, | 
|  | locale: object, | 
|  | optionManager: OptionManager | 
|  | ): void { | 
|  | theme = theme || {}; | 
|  | this.option = null; // Mark as not initialized. | 
|  | this._theme = new Model(theme); | 
|  | this._locale = new Model(locale); | 
|  | this._optionManager = optionManager; | 
|  | } | 
|  |  | 
|  | setOption( | 
|  | option: ECBasicOption, | 
|  | opts: GlobalModelSetOptionOpts, | 
|  | optionPreprocessorFuncs: OptionPreprocessor[] | 
|  | ): void { | 
|  |  | 
|  | if (__DEV__) { | 
|  | assert(option != null, 'option is null/undefined'); | 
|  | assert( | 
|  | option[OPTION_INNER_KEY] !== OPTION_INNER_VALUE, | 
|  | 'please use chart.getOption()' | 
|  | ); | 
|  | } | 
|  |  | 
|  | const innerOpt = normalizeSetOptionInput(opts); | 
|  |  | 
|  | this._optionManager.setOption(option, optionPreprocessorFuncs, innerOpt); | 
|  |  | 
|  | this._resetOption(null, innerOpt); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param type null/undefined: reset all. | 
|  | *        'recreate': force recreate all. | 
|  | *        'timeline': only reset timeline option | 
|  | *        'media': only reset media query option | 
|  | * @return Whether option changed. | 
|  | */ | 
|  | resetOption( | 
|  | type: 'recreate' | 'timeline' | 'media', | 
|  | opt?: Pick<GlobalModelSetOptionOpts, 'replaceMerge'> | 
|  | ): boolean { | 
|  | return this._resetOption(type, normalizeSetOptionInput(opt)); | 
|  | } | 
|  |  | 
|  | private _resetOption( | 
|  | type: 'recreate' | 'timeline' | 'media', | 
|  | opt: InnerSetOptionOpts | 
|  | ): boolean { | 
|  | let optionChanged = false; | 
|  | const optionManager = this._optionManager; | 
|  |  | 
|  | if (!type || type === 'recreate') { | 
|  | const baseOption = optionManager.mountOption(type === 'recreate'); | 
|  | if (__DEV__) { | 
|  | checkMissingComponents(baseOption); | 
|  | } | 
|  |  | 
|  | if (!this.option || type === 'recreate') { | 
|  | initBase(this, baseOption); | 
|  | } | 
|  | else { | 
|  | this.restoreData(); | 
|  | this._mergeOption(baseOption, opt); | 
|  | } | 
|  | optionChanged = true; | 
|  | } | 
|  |  | 
|  | if (type === 'timeline' || type === 'media') { | 
|  | this.restoreData(); | 
|  | } | 
|  |  | 
|  | // By design, if `setOption(option2)` at the second time, and `option2` is a `ECUnitOption`, | 
|  | // it should better not have the same props with `MediaUnit['option']`. | 
|  | // Because either `option2` or `MediaUnit['option']` will be always merged to "current option" | 
|  | // rather than original "baseOption". If they both override a prop, the result might be | 
|  | // unexpected when media state changed after `setOption` called. | 
|  | // If we really need to modify a props in each `MediaUnit['option']`, use the full version | 
|  | // (`{baseOption, media}`) in `setOption`. | 
|  | // For `timeline`, the case is the same. | 
|  |  | 
|  | if (!type || type === 'recreate' || type === 'timeline') { | 
|  | const timelineOption = optionManager.getTimelineOption(this); | 
|  | if (timelineOption) { | 
|  | optionChanged = true; | 
|  | this._mergeOption(timelineOption, opt); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (!type || type === 'recreate' || type === 'media') { | 
|  | const mediaOptions = optionManager.getMediaOption(this); | 
|  | if (mediaOptions.length) { | 
|  | each(mediaOptions, function (mediaOption) { | 
|  | optionChanged = true; | 
|  | this._mergeOption(mediaOption, opt); | 
|  | }, this); | 
|  | } | 
|  | } | 
|  |  | 
|  | return optionChanged; | 
|  | } | 
|  |  | 
|  | 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 componentsCount = this._componentsCount; | 
|  | const newCmptTypes: ComponentMainType[] = []; | 
|  | const newCmptTypeMap = createHashMap<boolean, string>(); | 
|  | const replaceMergeMainTypeMap = opt && opt.replaceMergeMainTypeMap; | 
|  |  | 
|  | resetSourceDefaulter(this); | 
|  |  | 
|  | // If no component class, merge directly. | 
|  | // For example: color, animaiton options, etc. | 
|  | each(newOption, function (componentOption, mainType: ComponentMainType) { | 
|  | if (componentOption == null) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (!ComponentModel.hasClass(mainType)) { | 
|  | // globalSettingTask.dirty(); | 
|  | option[mainType] = option[mainType] == null | 
|  | ? clone(componentOption) | 
|  | : merge(option[mainType], componentOption, true); | 
|  | } | 
|  | else if (mainType) { | 
|  | newCmptTypes.push(mainType); | 
|  | newCmptTypeMap.set(mainType, true); | 
|  | } | 
|  | }); | 
|  |  | 
|  | if (replaceMergeMainTypeMap) { | 
|  | // If there is a mainType `xxx` in `replaceMerge` but not declared in option, | 
|  | // we trade it as it is declared in option as `{xxx: []}`. Because: | 
|  | // (1) for normal merge, `{xxx: null/undefined}` are the same meaning as `{xxx: []}`. | 
|  | // (2) some preprocessor may convert some of `{xxx: null/undefined}` to `{xxx: []}`. | 
|  | replaceMergeMainTypeMap.each(function (val, mainTypeInReplaceMerge) { | 
|  | if (ComponentModel.hasClass(mainTypeInReplaceMerge) && !newCmptTypeMap.get(mainTypeInReplaceMerge)) { | 
|  | newCmptTypes.push(mainTypeInReplaceMerge); | 
|  | newCmptTypeMap.set(mainTypeInReplaceMerge, true); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | (ComponentModel as ComponentModelConstructor).topologicalTravel( | 
|  | newCmptTypes, | 
|  | (ComponentModel as ComponentModelConstructor).getAllClassMainTypes(), | 
|  | visitComponent, | 
|  | this | 
|  | ); | 
|  |  | 
|  | function visitComponent( | 
|  | this: GlobalModel, | 
|  | mainType: ComponentMainType | 
|  | ): void { | 
|  | const newCmptOptionList = concatInternalOptions( | 
|  | this, mainType, modelUtil.normalizeToArray(newOption[mainType]) | 
|  | ); | 
|  |  | 
|  | const oldCmptList = componentsMap.get(mainType); | 
|  | const mergeMode = | 
|  | // `!oldCmptList` means init. See the comment in `mappingToExists` | 
|  | !oldCmptList ? 'replaceAll' | 
|  | : (replaceMergeMainTypeMap && replaceMergeMainTypeMap.get(mainType)) ? 'replaceMerge' | 
|  | : 'normalMerge'; | 
|  | const mappingResult = modelUtil.mappingToExists(oldCmptList, newCmptOptionList, mergeMode); | 
|  |  | 
|  | // Set mainType and complete subType. | 
|  | modelUtil.setComponentTypeToKeyInfo(mappingResult, mainType, ComponentModel as ComponentModelConstructor); | 
|  |  | 
|  | // Empty it before the travel, in order to prevent `this._componentsMap` | 
|  | // from being used in the `init`/`mergeOption`/`optionUpdated` of some | 
|  | // components, which is probably incorrect logic. | 
|  | option[mainType] = null; | 
|  | componentsMap.set(mainType, null); | 
|  | componentsCount.set(mainType, 0); | 
|  |  | 
|  | const optionsByMainType = [] as ComponentOption[]; | 
|  | const cmptsByMainType = [] as ComponentModel[]; | 
|  | let cmptsCountByMainType = 0; | 
|  |  | 
|  | let tooltipExists: boolean; | 
|  | let tooltipWarningLogged: boolean; | 
|  |  | 
|  | each(mappingResult, function (resultItem, index) { | 
|  | 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 isSeriesType = mainType === 'series'; | 
|  | const ComponentModelClass = (ComponentModel as ComponentModelConstructor).getClass( | 
|  | mainType, resultItem.keyInfo.subType, | 
|  | !isSeriesType // Give a more detailed warn later if series don't exists | 
|  | ); | 
|  |  | 
|  | if (!ComponentModelClass) { | 
|  | if (__DEV__) { | 
|  | const subType = resultItem.keyInfo.subType; | 
|  | const seriesImportName = BUILTIN_CHARTS_MAP[subType as keyof typeof BUILTIN_CHARTS_MAP]; | 
|  | if (!componetsMissingLogPrinted[subType]) { | 
|  | componetsMissingLogPrinted[subType] = true; | 
|  | if (seriesImportName) { | 
|  | error(`Series ${subType} is used but not imported. | 
|  | import { ${seriesImportName} } from 'echarts/charts'; | 
|  | echarts.use([${seriesImportName}]);`); | 
|  | } | 
|  | else { | 
|  | error(`Unknown series ${subType}`); | 
|  | } | 
|  | } | 
|  | } | 
|  | return; | 
|  | } | 
|  |  | 
|  | // TODO Before multiple tooltips get supported, we do this check to avoid unexpected exception. | 
|  | if (mainType === 'tooltip') { | 
|  | if (tooltipExists) { | 
|  | if (__DEV__) { | 
|  | if (!tooltipWarningLogged) { | 
|  | warn('Currently only one tooltip component is allowed.'); | 
|  | tooltipWarningLogged = true; | 
|  | } | 
|  | } | 
|  | return; | 
|  | } | 
|  | tooltipExists = true; | 
|  | } | 
|  |  | 
|  | if (componentModel && componentModel.constructor === ComponentModelClass) { | 
|  | componentModel.name = resultItem.keyInfo.name; | 
|  | // componentModel.settingTask && componentModel.settingTask.dirty(); | 
|  | componentModel.mergeOption(newCmptOption, this); | 
|  | componentModel.optionUpdated(newCmptOption, false); | 
|  | } | 
|  | else { | 
|  | // PENDING Global as parent ? | 
|  | const extraOpt = extend( | 
|  | { | 
|  | componentIndex: index | 
|  | }, | 
|  | resultItem.keyInfo | 
|  | ); | 
|  | componentModel = new ComponentModelClass( | 
|  | newCmptOption, this, this, extraOpt | 
|  | ); | 
|  | // Assign `keyInfo` | 
|  | extend(componentModel, extraOpt); | 
|  | if (resultItem.brandNew) { | 
|  | componentModel.__requireNewView = true; | 
|  | } | 
|  | componentModel.init(newCmptOption, this, this); | 
|  |  | 
|  | // Call optionUpdated after init. | 
|  | // 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); | 
|  | } | 
|  | } | 
|  |  | 
|  | 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') { | 
|  | reCreateSeriesIndices(this); | 
|  | } | 
|  | } | 
|  |  | 
|  | // If no series declared, ensure `_seriesIndices` initialized. | 
|  | if (!this._seriesIndices) { | 
|  | reCreateSeriesIndices(this); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Get option for output (cloned option and inner info removed) | 
|  | */ | 
|  | getOption(): ECUnitOption { | 
|  | const option = clone(this.option); | 
|  |  | 
|  | each(option, function (optInMainType, mainType) { | 
|  | if (ComponentModel.hasClass(mainType)) { | 
|  | const opts = modelUtil.normalizeToArray(optInMainType); | 
|  | // 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 (opts[i] && !modelUtil.isComponentIdInternal(opts[i])) { | 
|  | metNonInner = true; | 
|  | } | 
|  | else { | 
|  | opts[i] = null; | 
|  | !metNonInner && realLen--; | 
|  | } | 
|  | } | 
|  | opts.length = realLen; | 
|  | option[mainType] = opts; | 
|  | } | 
|  | }); | 
|  |  | 
|  | delete option[OPTION_INNER_KEY]; | 
|  |  | 
|  | return option; | 
|  | } | 
|  |  | 
|  | getTheme(): Model { | 
|  | return this._theme; | 
|  | } | 
|  |  | 
|  | getLocaleModel(): Model<LocaleOption> { | 
|  | return this._locale; | 
|  | } | 
|  |  | 
|  | setUpdatePayload(payload: Payload) { | 
|  | this._payload = payload; | 
|  | } | 
|  |  | 
|  | getUpdatePayload(): Payload { | 
|  | return this._payload; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param idx If not specified, return the first one. | 
|  | */ | 
|  | getComponent(mainType: ComponentMainType, idx?: number): ComponentModel { | 
|  | const list = this._componentsMap.get(mainType); | 
|  | if (list) { | 
|  | const cmpt = list[idx || 0]; | 
|  | if (cmpt) { | 
|  | return cmpt; | 
|  | } | 
|  | else if (idx == null) { | 
|  | for (let i = 0; i < list.length; i++) { | 
|  | if (list[i]) { | 
|  | return list[i]; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return Never be null/undefined. | 
|  | */ | 
|  | queryComponents(condition: QueryConditionKindB): ComponentModel[] { | 
|  | const mainType = condition.mainType; | 
|  | if (!mainType) { | 
|  | return []; | 
|  | } | 
|  |  | 
|  | const index = condition.index; | 
|  | const id = condition.id; | 
|  | const name = condition.name; | 
|  | const cmpts = this._componentsMap.get(mainType); | 
|  |  | 
|  | if (!cmpts || !cmpts.length) { | 
|  | return []; | 
|  | } | 
|  |  | 
|  | let result: ComponentModel[]; | 
|  |  | 
|  | if (index != null) { | 
|  | result = []; | 
|  | each(modelUtil.normalizeToArray(index), function (idx) { | 
|  | cmpts[idx] && result.push(cmpts[idx]); | 
|  | }); | 
|  | } | 
|  | else if (id != null) { | 
|  | result = queryByIdOrName('id', id, cmpts); | 
|  | } | 
|  | else if (name != null) { | 
|  | result = queryByIdOrName('name', name, cmpts); | 
|  | } | 
|  | else { | 
|  | // Return all non-empty components in that mainType | 
|  | result = filter(cmpts, cmpt => !!cmpt); | 
|  | } | 
|  |  | 
|  | return filterBySubType(result, condition); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * The interface is different from queryComponents, | 
|  | * which is convenient for inner usage. | 
|  | * | 
|  | * @usage | 
|  | * let result = findComponents( | 
|  | *     {mainType: 'dataZoom', query: {dataZoomId: 'abc'}} | 
|  | * ); | 
|  | * let result = findComponents( | 
|  | *     {mainType: 'series', subType: 'pie', query: {seriesName: 'uio'}} | 
|  | * ); | 
|  | * let result = findComponents( | 
|  | *     {mainType: 'series', | 
|  | *     filter: function (model, index) {...}} | 
|  | * ); | 
|  | * // result like [component0, componnet1, ...] | 
|  | */ | 
|  | findComponents(condition: QueryConditionKindA): ComponentModel[] { | 
|  | const query = condition.query; | 
|  | const mainType = condition.mainType; | 
|  |  | 
|  | const queryCond = getQueryCond(query); | 
|  | const result = queryCond | 
|  | ? this.queryComponents(queryCond) | 
|  | // Retrieve all non-empty components. | 
|  | : filter(this._componentsMap.get(mainType), cmpt => !!cmpt); | 
|  |  | 
|  | return doFilter(filterBySubType(result, condition)); | 
|  |  | 
|  | function getQueryCond(q: QueryConditionKindA['query']): QueryConditionKindB { | 
|  | const indexAttr = mainType + 'Index'; | 
|  | const idAttr = mainType + 'Id'; | 
|  | const nameAttr = mainType + 'Name'; | 
|  | return q && ( | 
|  | q[indexAttr] != null | 
|  | || q[idAttr] != null | 
|  | || q[nameAttr] != null | 
|  | ) | 
|  | ? { | 
|  | mainType: mainType, | 
|  | // subType will be filtered finally. | 
|  | index: q[indexAttr] as (number | number[]), | 
|  | id: q[idAttr] as (OptionId | OptionId[]), | 
|  | name: q[nameAttr] as (OptionName | OptionName[]) | 
|  | } | 
|  | : null; | 
|  | } | 
|  |  | 
|  | function doFilter(res: ComponentModel[]) { | 
|  | return condition.filter | 
|  | ? filter(res, condition.filter) | 
|  | : res; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Travel components (before filtered). | 
|  | * | 
|  | * @usage | 
|  | * eachComponent('legend', function (legendModel, index) { | 
|  | *     ... | 
|  | * }); | 
|  | * eachComponent(function (componentType, model, index) { | 
|  | *     // componentType does not include subType | 
|  | *     // (componentType is 'a' but not 'a.b') | 
|  | * }); | 
|  | * eachComponent( | 
|  | *     {mainType: 'dataZoom', query: {dataZoomId: 'abc'}}, | 
|  | *     function (model, index) {...} | 
|  | * ); | 
|  | * eachComponent( | 
|  | *     {mainType: 'series', subType: 'pie', query: {seriesName: 'uio'}}, | 
|  | *     function (model, index) {...} | 
|  | * ); | 
|  | */ | 
|  | eachComponent<T>( | 
|  | cb: EachComponentAllCallback, | 
|  | context?: T | 
|  | ): void; | 
|  | eachComponent<T>( | 
|  | mainType: string, | 
|  | cb: EachComponentInMainTypeCallback, | 
|  | context?: T | 
|  | ): void; | 
|  | eachComponent<T>( | 
|  | mainType: QueryConditionKindA, | 
|  | cb: EachComponentInMainTypeCallback, | 
|  | context?: T | 
|  | ): void; | 
|  | eachComponent<T>( | 
|  | mainType: string | QueryConditionKindA | EachComponentAllCallback, | 
|  | cb?: EachComponentInMainTypeCallback | T, | 
|  | context?: T | 
|  | ) { | 
|  | const componentsMap = this._componentsMap; | 
|  |  | 
|  | 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 { | 
|  | 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: OptionName): SeriesModel[] { | 
|  | const nameStr = modelUtil.convertOptionIdName(name, null); | 
|  | return filter( | 
|  | this._componentsMap.get('series') as SeriesModel[], | 
|  | oneSeries => !!oneSeries && nameStr != null && oneSeries.name === nameStr | 
|  | ); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Get series list before filtered by index. | 
|  | */ | 
|  | getSeriesByIndex(seriesIndex: number): SeriesModel { | 
|  | return this._componentsMap.get('series')[seriesIndex] as SeriesModel; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Get series list before filtered by type. | 
|  | * FIXME: rename to getRawSeriesByType? | 
|  | */ | 
|  | getSeriesByType(subType: ComponentSubType): SeriesModel[] { | 
|  | return filter( | 
|  | this._componentsMap.get('series') as SeriesModel[], | 
|  | oneSeries => !!oneSeries && oneSeries.subType === subType | 
|  | ); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Get all series before filtered. | 
|  | */ | 
|  | getSeries(): SeriesModel[] { | 
|  | return filter( | 
|  | this._componentsMap.get('series') as SeriesModel[], | 
|  | oneSeries => !!oneSeries | 
|  | ); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Count series before filtered. | 
|  | */ | 
|  | getSeriesCount(): number { | 
|  | return this._componentsCount.get('series'); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * After filtering, series may be different | 
|  | * from raw series. | 
|  | */ | 
|  | eachSeries<T>( | 
|  | cb: (this: T, series: SeriesModel, rawSeriesIndex: number) => void, | 
|  | context?: T | 
|  | ): void { | 
|  | assertSeriesInitialized(this); | 
|  | each(this._seriesIndices, function (rawSeriesIndex) { | 
|  | const series = this._componentsMap.get('series')[rawSeriesIndex] as SeriesModel; | 
|  | cb.call(context, series, rawSeriesIndex); | 
|  | }, this); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Iterate raw series before filtered. | 
|  | * | 
|  | * @param {Function} cb | 
|  | * @param {*} context | 
|  | */ | 
|  | eachRawSeries<T>( | 
|  | cb: (this: T, series: SeriesModel, rawSeriesIndex: number) => void, | 
|  | context?: T | 
|  | ): void { | 
|  | each(this._componentsMap.get('series'), function (series) { | 
|  | series && cb.call(context, series, series.componentIndex); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * After filtering, series may be different. | 
|  | * from raw series. | 
|  | */ | 
|  | eachSeriesByType<T>( | 
|  | subType: ComponentSubType, | 
|  | cb: (this: T, series: SeriesModel, rawSeriesIndex: number) => void, | 
|  | context?: T | 
|  | ): void { | 
|  | assertSeriesInitialized(this); | 
|  | each(this._seriesIndices, function (rawSeriesIndex) { | 
|  | const series = this._componentsMap.get('series')[rawSeriesIndex] as SeriesModel; | 
|  | if (series.subType === subType) { | 
|  | cb.call(context, series, rawSeriesIndex); | 
|  | } | 
|  | }, this); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Iterate raw series before filtered of given type. | 
|  | */ | 
|  | eachRawSeriesByType<T>( | 
|  | subType: ComponentSubType, | 
|  | cb: (this: T, series: SeriesModel, rawSeriesIndex: number) => void, | 
|  | context?: T | 
|  | ): void { | 
|  | return each(this.getSeriesByType(subType), cb, context); | 
|  | } | 
|  |  | 
|  | isSeriesFiltered(seriesModel: SeriesModel): boolean { | 
|  | assertSeriesInitialized(this); | 
|  | return this._seriesIndicesMap.get(seriesModel.componentIndex) == null; | 
|  | } | 
|  |  | 
|  | getCurrentSeriesIndices(): number[] { | 
|  | return (this._seriesIndices || []).slice(); | 
|  | } | 
|  |  | 
|  | filterSeries<T>( | 
|  | cb: (this: T, series: SeriesModel, rawSeriesIndex: number) => boolean, | 
|  | context?: T | 
|  | ): void { | 
|  | assertSeriesInitialized(this); | 
|  |  | 
|  | 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 { | 
|  |  | 
|  | reCreateSeriesIndices(this); | 
|  |  | 
|  | const componentsMap = this._componentsMap; | 
|  | const componentTypes: string[] = []; | 
|  | componentsMap.each(function (components, componentType) { | 
|  | if (ComponentModel.hasClass(componentType)) { | 
|  | componentTypes.push(componentType); | 
|  | } | 
|  | }); | 
|  |  | 
|  | (ComponentModel as ComponentModelConstructor).topologicalTravel( | 
|  | componentTypes, | 
|  | (ComponentModel as ComponentModelConstructor).getAllClassMainTypes(), | 
|  | function (componentType) { | 
|  | each(componentsMap.get(componentType), function (component) { | 
|  | if (component | 
|  | && ( | 
|  | componentType !== 'series' | 
|  | || !isNotTargetSeries(component as SeriesModel, payload) | 
|  | ) | 
|  | ) { | 
|  | component.restoreData(); | 
|  | } | 
|  | }); | 
|  | } | 
|  | ); | 
|  | } | 
|  |  | 
|  | private static internalField = (function () { | 
|  |  | 
|  | 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 { | 
|  | // Components that use _seriesIndices should depends on series component, | 
|  | // which make sure that their initialization is after series. | 
|  | if (__DEV__) { | 
|  | if (!ecModel._seriesIndices) { | 
|  | throw new Error('Option should contains series.'); | 
|  | } | 
|  | } | 
|  | }; | 
|  |  | 
|  | initBase = function (ecModel: GlobalModel, baseOption: ECUnitOption & AriaOptionMixin): void { | 
|  | // Using OPTION_INNER_KEY to mark that this option cannot be used outside, | 
|  | // i.e. `chart.setOption(chart.getModel().option);` is forbidden. | 
|  | ecModel.option = {} as ECUnitOption; | 
|  | ecModel.option[OPTION_INNER_KEY] = OPTION_INNER_VALUE; | 
|  |  | 
|  | // Init with series: [], in case of calling findSeries method | 
|  | // before series initialized. | 
|  | ecModel._componentsMap = createHashMap({series: []}); | 
|  | ecModel._componentsCount = createHashMap(); | 
|  |  | 
|  | // If user spefied `option.aria`, aria will be enable. This detection should be | 
|  | // performed before theme and globalDefault merge. | 
|  | const airaOption = baseOption.aria; | 
|  | if (isObject(airaOption) && airaOption.enabled == null) { | 
|  | airaOption.enabled = true; | 
|  | } | 
|  |  | 
|  | mergeTheme(baseOption, ecModel._theme.option); | 
|  |  | 
|  | // TODO Needs clone when merging to the unexisted property | 
|  | merge(baseOption, globalDefault, false); | 
|  |  | 
|  | ecModel._mergeOption(baseOption, null); | 
|  | }; | 
|  |  | 
|  | })(); | 
|  | } | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @param condition.mainType Mandatory. | 
|  | * @param condition.subType Optional. | 
|  | * @param condition.query like {xxxIndex, xxxId, xxxName}, | 
|  | *        where xxx is mainType. | 
|  | *        If query attribute is null/undefined or has no index/id/name, | 
|  | *        do not filtering by query conditions, which is convenient for | 
|  | *        no-payload situations or when target of action is global. | 
|  | * @param condition.filter parameter: component, return boolean. | 
|  | */ | 
|  | export interface QueryConditionKindA { | 
|  | mainType: ComponentMainType; | 
|  | subType?: ComponentSubType; | 
|  | query?: { | 
|  | [k: string]: number | number[] | string | string[] | 
|  | }; | 
|  | filter?: (cmpt: ComponentModel) => boolean; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * If none of index and id and name used, return all components with mainType. | 
|  | * @param condition.mainType | 
|  | * @param condition.subType If ignore, only query by mainType | 
|  | * @param condition.index Either input index or id or name. | 
|  | * @param condition.id Either input index or id or name. | 
|  | * @param condition.name Either input index or id or name. | 
|  | */ | 
|  | export interface QueryConditionKindB { | 
|  | mainType: ComponentMainType; | 
|  | subType?: ComponentSubType; | 
|  | index?: number | number[]; | 
|  | id?: OptionId | OptionId[]; | 
|  | name?: OptionName | OptionName[]; | 
|  | } | 
|  | export interface EachComponentAllCallback { | 
|  | (mainType: string, model: ComponentModel, componentIndex: number): void; | 
|  | } | 
|  | interface EachComponentInMainTypeCallback { | 
|  | (model: ComponentModel, componentIndex: number): void; | 
|  | } | 
|  |  | 
|  |  | 
|  | function isNotTargetSeries(seriesModel: SeriesModel, payload: Payload): boolean { | 
|  | if (payload) { | 
|  | const index = payload.seriesIndex; | 
|  | const id = payload.seriesId; | 
|  | const name = payload.seriesName; | 
|  | return (index != null && seriesModel.componentIndex !== index) | 
|  | || (id != null && seriesModel.id !== id) | 
|  | || (name != null && seriesModel.name !== name); | 
|  | } | 
|  | } | 
|  |  | 
|  | function mergeTheme(option: ECUnitOption, theme: ThemeOption): void { | 
|  | // PENDING | 
|  | // NOT use `colorLayer` in theme if option has `color` | 
|  | const notMergeColorLayer = option.color && !option.colorLayer; | 
|  |  | 
|  | each(theme, function (themeItem, name) { | 
|  | if (name === 'colorLayer' && notMergeColorLayer) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // If it is component model mainType, the model handles that merge later. | 
|  | // otherwise, merge them here. | 
|  | if (!ComponentModel.hasClass(name)) { | 
|  | if (typeof themeItem === 'object') { | 
|  | option[name] = !option[name] | 
|  | ? clone(themeItem) | 
|  | : merge(option[name], themeItem, false); | 
|  | } | 
|  | else { | 
|  | if (option[name] == null) { | 
|  | option[name] = themeItem; | 
|  | } | 
|  | } | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | function queryByIdOrName<T extends { id?: string, name?: string }>( | 
|  | attr: 'id' | 'name', | 
|  | idOrName: string | number | (string | number)[], | 
|  | cmpts: T[] | 
|  | ): T[] { | 
|  | // Here is a break from echarts4: string and number are | 
|  | // treated as equal. | 
|  | if (isArray(idOrName)) { | 
|  | const keyMap = createHashMap<boolean>(); | 
|  | each(idOrName, function (idOrNameItem) { | 
|  | if (idOrNameItem != null) { | 
|  | const idName = modelUtil.convertOptionIdName(idOrNameItem, null); | 
|  | idName != null && keyMap.set(idOrNameItem, true); | 
|  | } | 
|  | }); | 
|  | return filter(cmpts, cmpt => cmpt && keyMap.get(cmpt[attr])); | 
|  | } | 
|  | else { | 
|  | const idName = modelUtil.convertOptionIdName(idOrName, null); | 
|  | return filter(cmpts, cmpt => cmpt && idName != null && cmpt[attr] === idName); | 
|  | } | 
|  | } | 
|  |  | 
|  | function filterBySubType( | 
|  | components: ComponentModel[], | 
|  | condition: QueryConditionKindA | QueryConditionKindB | 
|  | ): ComponentModel[] { | 
|  | // Using hasOwnProperty for restrict. Consider | 
|  | // subType is undefined in user payload. | 
|  | return condition.hasOwnProperty('subType') | 
|  | ? filter(components, cmpt => cmpt && cmpt.subType === condition.subType) | 
|  | : components; | 
|  | } | 
|  |  | 
|  | function normalizeSetOptionInput(opts: GlobalModelSetOptionOpts): InnerSetOptionOpts { | 
|  | const replaceMergeMainTypeMap = createHashMap<boolean, string>(); | 
|  | opts && each(modelUtil.normalizeToArray(opts.replaceMerge), function (mainType) { | 
|  | if (__DEV__) { | 
|  | assert( | 
|  | ComponentModel.hasClass(mainType), | 
|  | '"' + mainType + '" is not valid component main type in "replaceMerge"' | 
|  | ); | 
|  | } | 
|  | replaceMergeMainTypeMap.set(mainType, true); | 
|  | }); | 
|  | return { | 
|  | replaceMergeMainTypeMap: replaceMergeMainTypeMap | 
|  | }; | 
|  | } | 
|  |  | 
|  | interface GlobalModel extends PaletteMixin<ECUnitOption> {} | 
|  | mixin(GlobalModel, PaletteMixin); | 
|  |  | 
|  | export default GlobalModel; |