| /* | |
| * 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; |