Spaces:
Running
Running
| /* | |
| * 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. | |
| */ | |
| /** | |
| * ECharts option manager | |
| */ | |
| // import ComponentModel, { ComponentModelConstructor } from './Component'; | |
| import ExtensionAPI from '../core/ExtensionAPI'; | |
| import { | |
| OptionPreprocessor, MediaQuery, ECUnitOption, MediaUnit, ECBasicOption, SeriesOption | |
| } from '../util/types'; | |
| import GlobalModel, { InnerSetOptionOpts } from './Global'; | |
| import { | |
| normalizeToArray | |
| // , MappingExistingItem, setComponentTypeToKeyInfo, mappingToExists | |
| } from '../util/model'; | |
| import { | |
| each, clone, map, isTypedArray, setAsPrimitive, isArray, isObject | |
| // , HashMap , createHashMap, extend, merge, | |
| } from 'zrender/src/core/util'; | |
| import { DatasetOption } from '../component/dataset/install'; | |
| import { error } from '../util/log'; | |
| const QUERY_REG = /^(min|max)?(.+)$/; | |
| interface ParsedRawOption { | |
| baseOption: ECUnitOption; | |
| timelineOptions: ECUnitOption[]; | |
| mediaDefault: MediaUnit; | |
| mediaList: MediaUnit[]; | |
| } | |
| // Key: mainType | |
| // type FakeComponentsMap = HashMap<(MappingExistingItem & { subType: string })[]>; | |
| /** | |
| * TERM EXPLANATIONS: | |
| * See `ECOption` and `ECUnitOption` in `src/util/types.ts`. | |
| */ | |
| class OptionManager { | |
| private _api: ExtensionAPI; | |
| private _timelineOptions: ECUnitOption[] = []; | |
| private _mediaList: MediaUnit[] = []; | |
| private _mediaDefault: MediaUnit; | |
| /** | |
| * -1, means default. | |
| * empty means no media. | |
| */ | |
| private _currentMediaIndices: number[] = []; | |
| private _optionBackup: ParsedRawOption; | |
| // private _fakeCmptsMap: FakeComponentsMap; | |
| private _newBaseOption: ECUnitOption; | |
| // timeline.notMerge is not supported in ec3. Firstly there is rearly | |
| // case that notMerge is needed. Secondly supporting 'notMerge' requires | |
| // rawOption cloned and backuped when timeline changed, which does no | |
| // good to performance. What's more, that both timeline and setOption | |
| // method supply 'notMerge' brings complex and some problems. | |
| // Consider this case: | |
| // (step1) chart.setOption({timeline: {notMerge: false}, ...}, false); | |
| // (step2) chart.setOption({timeline: {notMerge: true}, ...}, false); | |
| constructor(api: ExtensionAPI) { | |
| this._api = api; | |
| } | |
| setOption( | |
| rawOption: ECBasicOption, | |
| optionPreprocessorFuncs: OptionPreprocessor[], | |
| opt: InnerSetOptionOpts | |
| ): void { | |
| if (rawOption) { | |
| // That set dat primitive is dangerous if user reuse the data when setOption again. | |
| each(normalizeToArray((rawOption as ECUnitOption).series), function (series: SeriesOption) { | |
| series && series.data && isTypedArray(series.data) && setAsPrimitive(series.data); | |
| }); | |
| each(normalizeToArray((rawOption as ECUnitOption).dataset), function (dataset: DatasetOption) { | |
| dataset && dataset.source && isTypedArray(dataset.source) && setAsPrimitive(dataset.source); | |
| }); | |
| } | |
| // Caution: some series modify option data, if do not clone, | |
| // it should ensure that the repeat modify correctly | |
| // (create a new object when modify itself). | |
| rawOption = clone(rawOption); | |
| // FIXME | |
| // If some property is set in timeline options or media option but | |
| // not set in baseOption, a warning should be given. | |
| const optionBackup = this._optionBackup; | |
| const newParsedOption = parseRawOption( | |
| rawOption, optionPreprocessorFuncs, !optionBackup | |
| ); | |
| this._newBaseOption = newParsedOption.baseOption; | |
| // For setOption at second time (using merge mode); | |
| if (optionBackup) { | |
| // FIXME | |
| // the restore merge solution is essentially incorrect. | |
| // the mapping can not be 100% consistent with ecModel, which probably brings | |
| // potential bug! | |
| // The first merge is delayed, because in most cases, users do not call `setOption` twice. | |
| // let fakeCmptsMap = this._fakeCmptsMap; | |
| // if (!fakeCmptsMap) { | |
| // fakeCmptsMap = this._fakeCmptsMap = createHashMap(); | |
| // mergeToBackupOption(fakeCmptsMap, null, optionBackup.baseOption, null); | |
| // } | |
| // mergeToBackupOption( | |
| // fakeCmptsMap, optionBackup.baseOption, newParsedOption.baseOption, opt | |
| // ); | |
| // For simplicity, timeline options and media options do not support merge, | |
| // that is, if you `setOption` twice and both has timeline options, the latter | |
| // timeline options will not be merged to the former, but just substitute them. | |
| if (newParsedOption.timelineOptions.length) { | |
| optionBackup.timelineOptions = newParsedOption.timelineOptions; | |
| } | |
| if (newParsedOption.mediaList.length) { | |
| optionBackup.mediaList = newParsedOption.mediaList; | |
| } | |
| if (newParsedOption.mediaDefault) { | |
| optionBackup.mediaDefault = newParsedOption.mediaDefault; | |
| } | |
| } | |
| else { | |
| this._optionBackup = newParsedOption; | |
| } | |
| } | |
| mountOption(isRecreate: boolean): ECUnitOption { | |
| const optionBackup = this._optionBackup; | |
| this._timelineOptions = optionBackup.timelineOptions; | |
| this._mediaList = optionBackup.mediaList; | |
| this._mediaDefault = optionBackup.mediaDefault; | |
| this._currentMediaIndices = []; | |
| return clone(isRecreate | |
| // this._optionBackup.baseOption, which is created at the first `setOption` | |
| // called, and is merged into every new option by inner method `mergeToBackupOption` | |
| // each time `setOption` called, can be only used in `isRecreate`, because | |
| // its reliability is under suspicion. In other cases option merge is | |
| // performed by `model.mergeOption`. | |
| ? optionBackup.baseOption : this._newBaseOption | |
| ); | |
| } | |
| getTimelineOption(ecModel: GlobalModel): ECUnitOption { | |
| let option; | |
| const timelineOptions = this._timelineOptions; | |
| if (timelineOptions.length) { | |
| // getTimelineOption can only be called after ecModel inited, | |
| // so we can get currentIndex from timelineModel. | |
| const timelineModel = ecModel.getComponent('timeline'); | |
| if (timelineModel) { | |
| option = clone( | |
| // FIXME:TS as TimelineModel or quivlant interface | |
| timelineOptions[(timelineModel as any).getCurrentIndex()] | |
| ); | |
| } | |
| } | |
| return option; | |
| } | |
| getMediaOption(ecModel: GlobalModel): ECUnitOption[] { | |
| const ecWidth = this._api.getWidth(); | |
| const ecHeight = this._api.getHeight(); | |
| const mediaList = this._mediaList; | |
| const mediaDefault = this._mediaDefault; | |
| let indices = []; | |
| let result: ECUnitOption[] = []; | |
| // No media defined. | |
| if (!mediaList.length && !mediaDefault) { | |
| return result; | |
| } | |
| // Multi media may be applied, the latter defined media has higher priority. | |
| for (let i = 0, len = mediaList.length; i < len; i++) { | |
| if (applyMediaQuery(mediaList[i].query, ecWidth, ecHeight)) { | |
| indices.push(i); | |
| } | |
| } | |
| // FIXME | |
| // Whether mediaDefault should force users to provide? Otherwise | |
| // the change by media query can not be recorvered. | |
| if (!indices.length && mediaDefault) { | |
| indices = [-1]; | |
| } | |
| if (indices.length && !indicesEquals(indices, this._currentMediaIndices)) { | |
| result = map(indices, function (index) { | |
| return clone( | |
| index === -1 ? mediaDefault.option : mediaList[index].option | |
| ); | |
| }); | |
| } | |
| // Otherwise return nothing. | |
| this._currentMediaIndices = indices; | |
| return result; | |
| } | |
| } | |
| /** | |
| * [RAW_OPTION_PATTERNS] | |
| * (Note: "series: []" represents all other props in `ECUnitOption`) | |
| * | |
| * (1) No prop "baseOption" declared: | |
| * Root option is used as "baseOption" (except prop "options" and "media"). | |
| * ```js | |
| * option = { | |
| * series: [], | |
| * timeline: {}, | |
| * options: [], | |
| * }; | |
| * option = { | |
| * series: [], | |
| * media: {}, | |
| * }; | |
| * option = { | |
| * series: [], | |
| * timeline: {}, | |
| * options: [], | |
| * media: {}, | |
| * } | |
| * ``` | |
| * | |
| * (2) Prop "baseOption" declared: | |
| * If "baseOption" declared, `ECUnitOption` props can only be declared | |
| * inside "baseOption" except prop "timeline" (compat ec2). | |
| * ```js | |
| * option = { | |
| * baseOption: { | |
| * timeline: {}, | |
| * series: [], | |
| * }, | |
| * options: [] | |
| * }; | |
| * option = { | |
| * baseOption: { | |
| * series: [], | |
| * }, | |
| * media: [] | |
| * }; | |
| * option = { | |
| * baseOption: { | |
| * timeline: {}, | |
| * series: [], | |
| * }, | |
| * options: [] | |
| * media: [] | |
| * }; | |
| * option = { | |
| * // ec3 compat ec2: allow (only) `timeline` declared | |
| * // outside baseOption. Keep this setting for compat. | |
| * timeline: {}, | |
| * baseOption: { | |
| * series: [], | |
| * }, | |
| * options: [], | |
| * media: [] | |
| * }; | |
| * ``` | |
| */ | |
| function parseRawOption( | |
| // `rawOption` May be modified | |
| rawOption: ECBasicOption, | |
| optionPreprocessorFuncs: OptionPreprocessor[], | |
| isNew: boolean | |
| ): ParsedRawOption { | |
| const mediaList: MediaUnit[] = []; | |
| let mediaDefault: MediaUnit; | |
| let baseOption: ECUnitOption; | |
| const declaredBaseOption = rawOption.baseOption; | |
| // Compatible with ec2, [RAW_OPTION_PATTERNS] above. | |
| const timelineOnRoot = rawOption.timeline; | |
| const timelineOptionsOnRoot = rawOption.options; | |
| const mediaOnRoot = rawOption.media; | |
| const hasMedia = !!rawOption.media; | |
| const hasTimeline = !!( | |
| timelineOptionsOnRoot || timelineOnRoot || (declaredBaseOption && declaredBaseOption.timeline) | |
| ); | |
| if (declaredBaseOption) { | |
| baseOption = declaredBaseOption; | |
| // For merge option. | |
| if (!baseOption.timeline) { | |
| baseOption.timeline = timelineOnRoot; | |
| } | |
| } | |
| // For convenience, enable to use the root option as the `baseOption`: | |
| // `{ ...normalOptionProps, media: [{ ... }, { ... }] }` | |
| else { | |
| if (hasTimeline || hasMedia) { | |
| rawOption.options = rawOption.media = null; | |
| } | |
| baseOption = rawOption; | |
| } | |
| if (hasMedia) { | |
| if (isArray(mediaOnRoot)) { | |
| each(mediaOnRoot, function (singleMedia) { | |
| if (__DEV__) { | |
| // Real case of wrong config. | |
| if (singleMedia | |
| && !singleMedia.option | |
| && isObject(singleMedia.query) | |
| && isObject((singleMedia.query as any).option) | |
| ) { | |
| error('Illegal media option. Must be like { media: [ { query: {}, option: {} } ] }'); | |
| } | |
| } | |
| if (singleMedia && singleMedia.option) { | |
| if (singleMedia.query) { | |
| mediaList.push(singleMedia); | |
| } | |
| else if (!mediaDefault) { | |
| // Use the first media default. | |
| mediaDefault = singleMedia; | |
| } | |
| } | |
| }); | |
| } | |
| else { | |
| if (__DEV__) { | |
| // Real case of wrong config. | |
| error('Illegal media option. Must be an array. Like { media: [ {...}, {...} ] }'); | |
| } | |
| } | |
| } | |
| doPreprocess(baseOption); | |
| each(timelineOptionsOnRoot, option => doPreprocess(option)); | |
| each(mediaList, media => doPreprocess(media.option)); | |
| function doPreprocess(option: ECUnitOption) { | |
| each(optionPreprocessorFuncs, function (preProcess) { | |
| preProcess(option, isNew); | |
| }); | |
| } | |
| return { | |
| baseOption: baseOption, | |
| timelineOptions: timelineOptionsOnRoot || [], | |
| mediaDefault: mediaDefault, | |
| mediaList: mediaList | |
| }; | |
| } | |
| /** | |
| * @see <http://www.w3.org/TR/css3-mediaqueries/#media1> | |
| * Support: width, height, aspectRatio | |
| * Can use max or min as prefix. | |
| */ | |
| function applyMediaQuery(query: MediaQuery, ecWidth: number, ecHeight: number): boolean { | |
| const realMap = { | |
| width: ecWidth, | |
| height: ecHeight, | |
| aspectratio: ecWidth / ecHeight // lower case for convenience. | |
| }; | |
| let applicable = true; | |
| each(query, function (value: number, attr) { | |
| const matched = attr.match(QUERY_REG); | |
| if (!matched || !matched[1] || !matched[2]) { | |
| return; | |
| } | |
| const operator = matched[1]; | |
| const realAttr = matched[2].toLowerCase(); | |
| if (!compare(realMap[realAttr as keyof typeof realMap], value, operator)) { | |
| applicable = false; | |
| } | |
| }); | |
| return applicable; | |
| } | |
| function compare(real: number, expect: number, operator: string): boolean { | |
| if (operator === 'min') { | |
| return real >= expect; | |
| } | |
| else if (operator === 'max') { | |
| return real <= expect; | |
| } | |
| else { // Equals | |
| return real === expect; | |
| } | |
| } | |
| function indicesEquals(indices1: number[], indices2: number[]): boolean { | |
| // indices is always order by asc and has only finite number. | |
| return indices1.join(',') === indices2.join(','); | |
| } | |
| /** | |
| * Consider case: | |
| * `chart.setOption(opt1);` | |
| * Then user do some interaction like dataZoom, dataView changing. | |
| * `chart.setOption(opt2);` | |
| * Then user press 'reset button' in toolbox. | |
| * | |
| * After doing that all of the interaction effects should be reset, the | |
| * chart should be the same as the result of invoke | |
| * `chart.setOption(opt1); chart.setOption(opt2);`. | |
| * | |
| * Although it is not able ensure that | |
| * `chart.setOption(opt1); chart.setOption(opt2);` is equivalents to | |
| * `chart.setOption(merge(opt1, opt2));` exactly, | |
| * this might be the only simple way to implement that feature. | |
| * | |
| * MEMO: We've considered some other approaches: | |
| * 1. Each model handles its self restoration but not uniform treatment. | |
| * (Too complex in logic and error-prone) | |
| * 2. Use a shadow ecModel. (Performance expensive) | |
| * | |
| * FIXME: A possible solution: | |
| * Add a extra level of model for each component model. The inheritance chain would be: | |
| * ecModel <- componentModel <- componentActionModel <- dataItemModel | |
| * And all of the actions can only modify the `componentActionModel` rather than | |
| * `componentModel`. `setOption` will only modify the `ecModel` and `componentModel`. | |
| * When "resotre" action triggered, model from `componentActionModel` will be discarded | |
| * instead of recreating the "ecModel" from the "_optionBackup". | |
| */ | |
| // function mergeToBackupOption( | |
| // fakeCmptsMap: FakeComponentsMap, | |
| // // `tarOption` Can be null/undefined, means init | |
| // tarOption: ECUnitOption, | |
| // newOption: ECUnitOption, | |
| // // Can be null/undefined | |
| // opt: InnerSetOptionOpts | |
| // ): void { | |
| // newOption = newOption || {} as ECUnitOption; | |
| // const notInit = !!tarOption; | |
| // each(newOption, function (newOptsInMainType, mainType) { | |
| // if (newOptsInMainType == null) { | |
| // return; | |
| // } | |
| // if (!ComponentModel.hasClass(mainType)) { | |
| // if (tarOption) { | |
| // tarOption[mainType] = merge(tarOption[mainType], newOptsInMainType, true); | |
| // } | |
| // } | |
| // else { | |
| // const oldTarOptsInMainType = notInit ? normalizeToArray(tarOption[mainType]) : null; | |
| // const oldFakeCmptsInMainType = fakeCmptsMap.get(mainType) || []; | |
| // const resultTarOptsInMainType = notInit ? (tarOption[mainType] = [] as ComponentOption[]) : null; | |
| // const resultFakeCmptsInMainType = fakeCmptsMap.set(mainType, []); | |
| // const mappingResult = mappingToExists( | |
| // oldFakeCmptsInMainType, | |
| // normalizeToArray(newOptsInMainType), | |
| // (opt && opt.replaceMergeMainTypeMap.get(mainType)) ? 'replaceMerge' : 'normalMerge' | |
| // ); | |
| // setComponentTypeToKeyInfo(mappingResult, mainType, ComponentModel as ComponentModelConstructor); | |
| // each(mappingResult, function (resultItem, index) { | |
| // // The same logic as `Global.ts#_mergeOption`. | |
| // let fakeCmpt = resultItem.existing; | |
| // const newOption = resultItem.newOption; | |
| // const keyInfo = resultItem.keyInfo; | |
| // let fakeCmptOpt; | |
| // if (!newOption) { | |
| // fakeCmptOpt = oldTarOptsInMainType[index]; | |
| // } | |
| // else { | |
| // if (fakeCmpt && fakeCmpt.subType === keyInfo.subType) { | |
| // fakeCmpt.name = keyInfo.name; | |
| // if (notInit) { | |
| // fakeCmptOpt = merge(oldTarOptsInMainType[index], newOption, true); | |
| // } | |
| // } | |
| // else { | |
| // fakeCmpt = extend({}, keyInfo); | |
| // if (notInit) { | |
| // fakeCmptOpt = clone(newOption); | |
| // } | |
| // } | |
| // } | |
| // if (fakeCmpt) { | |
| // notInit && resultTarOptsInMainType.push(fakeCmptOpt); | |
| // resultFakeCmptsInMainType.push(fakeCmpt); | |
| // } | |
| // else { | |
| // notInit && resultTarOptsInMainType.push(void 0); | |
| // resultFakeCmptsInMainType.push(void 0); | |
| // } | |
| // }); | |
| // } | |
| // }); | |
| // } | |
| export default OptionManager; | |