You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
491 lines
17 KiB
491 lines
17 KiB
|
|
/* |
|
* 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. |
|
*/ |
|
|
|
|
|
/** |
|
* AUTO-GENERATED FILE. DO NOT MODIFY. |
|
*/ |
|
|
|
/* |
|
* 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. |
|
*/ |
|
import { normalizeToArray // , MappingExistingItem, setComponentTypeToKeyInfo, mappingToExists |
|
} from '../util/model.js'; |
|
import { each, clone, map, isTypedArray, setAsPrimitive, isArray, isObject // , HashMap , createHashMap, extend, merge, |
|
} from 'zrender/lib/core/util.js'; |
|
import { error } from '../util/log.js'; |
|
var QUERY_REG = /^(min|max)?(.+)$/; // Key: mainType |
|
// type FakeComponentsMap = HashMap<(MappingExistingItem & { subType: string })[]>; |
|
|
|
/** |
|
* TERM EXPLANATIONS: |
|
* See `ECOption` and `ECUnitOption` in `src/util/types.ts`. |
|
*/ |
|
|
|
var OptionManager = |
|
/** @class */ |
|
function () { |
|
// 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); |
|
function OptionManager(api) { |
|
this._timelineOptions = []; |
|
this._mediaList = []; |
|
/** |
|
* -1, means default. |
|
* empty means no media. |
|
*/ |
|
|
|
this._currentMediaIndices = []; |
|
this._api = api; |
|
} |
|
|
|
OptionManager.prototype.setOption = function (rawOption, optionPreprocessorFuncs, opt) { |
|
if (rawOption) { |
|
// That set dat primitive is dangerous if user reuse the data when setOption again. |
|
each(normalizeToArray(rawOption.series), function (series) { |
|
series && series.data && isTypedArray(series.data) && setAsPrimitive(series.data); |
|
}); |
|
each(normalizeToArray(rawOption.dataset), function (dataset) { |
|
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. |
|
|
|
var optionBackup = this._optionBackup; |
|
var 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; |
|
} |
|
}; |
|
|
|
OptionManager.prototype.mountOption = function (isRecreate) { |
|
var 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); |
|
}; |
|
|
|
OptionManager.prototype.getTimelineOption = function (ecModel) { |
|
var option; |
|
var timelineOptions = this._timelineOptions; |
|
|
|
if (timelineOptions.length) { |
|
// getTimelineOption can only be called after ecModel inited, |
|
// so we can get currentIndex from timelineModel. |
|
var timelineModel = ecModel.getComponent('timeline'); |
|
|
|
if (timelineModel) { |
|
option = clone( // FIXME:TS as TimelineModel or quivlant interface |
|
timelineOptions[timelineModel.getCurrentIndex()]); |
|
} |
|
} |
|
|
|
return option; |
|
}; |
|
|
|
OptionManager.prototype.getMediaOption = function (ecModel) { |
|
var ecWidth = this._api.getWidth(); |
|
|
|
var ecHeight = this._api.getHeight(); |
|
|
|
var mediaList = this._mediaList; |
|
var mediaDefault = this._mediaDefault; |
|
var indices = []; |
|
var result = []; // No media defined. |
|
|
|
if (!mediaList.length && !mediaDefault) { |
|
return result; |
|
} // Multi media may be applied, the latter defined media has higher priority. |
|
|
|
|
|
for (var 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; |
|
}; |
|
|
|
return OptionManager; |
|
}(); |
|
/** |
|
* [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, optionPreprocessorFuncs, isNew) { |
|
var mediaList = []; |
|
var mediaDefault; |
|
var baseOption; |
|
var declaredBaseOption = rawOption.baseOption; // Compatible with ec2, [RAW_OPTION_PATTERNS] above. |
|
|
|
var timelineOnRoot = rawOption.timeline; |
|
var timelineOptionsOnRoot = rawOption.options; |
|
var mediaOnRoot = rawOption.media; |
|
var hasMedia = !!rawOption.media; |
|
var 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 (process.env.NODE_ENV !== 'production') { |
|
// Real case of wrong config. |
|
if (singleMedia && !singleMedia.option && isObject(singleMedia.query) && isObject(singleMedia.query.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 (process.env.NODE_ENV !== 'production') { |
|
// Real case of wrong config. |
|
error('Illegal media option. Must be an array. Like { media: [ {...}, {...} ] }'); |
|
} |
|
} |
|
} |
|
|
|
doPreprocess(baseOption); |
|
each(timelineOptionsOnRoot, function (option) { |
|
return doPreprocess(option); |
|
}); |
|
each(mediaList, function (media) { |
|
return doPreprocess(media.option); |
|
}); |
|
|
|
function doPreprocess(option) { |
|
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, ecWidth, ecHeight) { |
|
var realMap = { |
|
width: ecWidth, |
|
height: ecHeight, |
|
aspectratio: ecWidth / ecHeight // lower case for convenience. |
|
|
|
}; |
|
var applicable = true; |
|
each(query, function (value, attr) { |
|
var matched = attr.match(QUERY_REG); |
|
|
|
if (!matched || !matched[1] || !matched[2]) { |
|
return; |
|
} |
|
|
|
var operator = matched[1]; |
|
var realAttr = matched[2].toLowerCase(); |
|
|
|
if (!compare(realMap[realAttr], value, operator)) { |
|
applicable = false; |
|
} |
|
}); |
|
return applicable; |
|
} |
|
|
|
function compare(real, expect, operator) { |
|
if (operator === 'min') { |
|
return real >= expect; |
|
} else if (operator === 'max') { |
|
return real <= expect; |
|
} else { |
|
// Equals |
|
return real === expect; |
|
} |
|
} |
|
|
|
function indicesEquals(indices1, indices2) { |
|
// 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; |