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.
458 lines
17 KiB
458 lines
17 KiB
1 month ago
|
|
||
|
/*
|
||
|
* 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;
|