echarts OptionManager 源码

2022-10-20 浏览 (668)

echarts OptionManager 代码

文件路径:/src/model/OptionManager.ts

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

相关信息

echarts 源码目录

相关文章

echarts Component 源码

echarts Global 源码

echarts Model 源码

echarts Series 源码

echarts globalDefault 源码

echarts internalComponentCreator 源码

echarts referHelper 源码

^