| /* |
| * 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" 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. |
| * 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']`. |
| // Becuase 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(`Unkown 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 |
| * frome 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. |
| * frome 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 can not be used outside, |
| // i.e. `chart.setOption(chart.getModel().option);` is forbiden. |
| 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; |