/* * 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. */ var _config = require("../config"); var __DEV__ = _config.__DEV__; var _util = require("zrender/lib/core/util"); var each = _util.each; var filter = _util.filter; var map = _util.map; var isArray = _util.isArray; var indexOf = _util.indexOf; var isObject = _util.isObject; var isString = _util.isString; var createHashMap = _util.createHashMap; var assert = _util.assert; var clone = _util.clone; var merge = _util.merge; var extend = _util.extend; var mixin = _util.mixin; var modelUtil = require("../util/model"); var Model = require("./Model"); var ComponentModel = require("./Component"); var globalDefault = require("./globalDefault"); var colorPaletteMixin = require("./mixin/colorPalette"); var _sourceHelper = require("../data/helper/sourceHelper"); var resetSourceDefaulter = _sourceHelper.resetSourceDefaulter; /* * 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 global model * * @module {echarts/model/Global} */ /** * 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) `reset` feature (in toolbox). Find detailed info in comments about * `mergeOption` in module:echarts/model/OptionManager. */ var OPTION_INNER_KEY = '\0_ec_inner'; /** * @alias module:echarts/model/Global * * @param {Object} option * @param {module:echarts/model/Model} parentModel * @param {Object} theme */ var GlobalModel = Model.extend({ init: function (option, parentModel, theme, optionManager) { theme = theme || {}; this.option = null; // Mark as not initialized. /** * @type {module:echarts/model/Model} * @private */ this._theme = new Model(theme); /** * @type {module:echarts/model/OptionManager} */ this._optionManager = optionManager; }, setOption: function (option, optionPreprocessorFuncs) { assert(!(OPTION_INNER_KEY in option), 'please use chart.getOption()'); this._optionManager.setOption(option, optionPreprocessorFuncs); this.resetOption(null); }, /** * @param {string} type null/undefined: reset all. * 'recreate': force recreate all. * 'timeline': only reset timeline option * 'media': only reset media query option * @return {boolean} Whether option changed. */ resetOption: function (type) { var optionChanged = false; var optionManager = this._optionManager; if (!type || type === 'recreate') { var baseOption = optionManager.mountOption(type === 'recreate'); if (!this.option || type === 'recreate') { initBase.call(this, baseOption); } else { this.restoreData(); this.mergeOption(baseOption); } optionChanged = true; } if (type === 'timeline' || type === 'media') { this.restoreData(); } if (!type || type === 'recreate' || type === 'timeline') { var timelineOption = optionManager.getTimelineOption(this); timelineOption && (this.mergeOption(timelineOption), optionChanged = true); } if (!type || type === 'recreate' || type === 'media') { var mediaOptions = optionManager.getMediaOption(this, this._api); if (mediaOptions.length) { each(mediaOptions, function (mediaOption) { this.mergeOption(mediaOption, optionChanged = true); }, this); } } return optionChanged; }, /** * @protected */ mergeOption: function (newOption) { var option = this.option; var componentsMap = this._componentsMap; var newCptTypes = []; resetSourceDefaulter(this); // If no component class, merge directly. // For example: color, animaiton options, etc. each(newOption, function (componentOption, mainType) { 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) { newCptTypes.push(mainType); } }); ComponentModel.topologicalTravel(newCptTypes, ComponentModel.getAllClassMainTypes(), visitComponent, this); function visitComponent(mainType, dependencies) { var newCptOptionList = modelUtil.normalizeToArray(newOption[mainType]); var mapResult = modelUtil.mappingToExists(componentsMap.get(mainType), newCptOptionList); modelUtil.makeIdAndName(mapResult); // Set mainType and complete subType. each(mapResult, function (item, index) { var opt = item.option; if (isObject(opt)) { item.keyInfo.mainType = mainType; item.keyInfo.subType = determineSubType(mainType, opt, item.exist); } }); var dependentModels = getComponentsByTypes(componentsMap, dependencies); option[mainType] = []; componentsMap.set(mainType, []); each(mapResult, function (resultItem, index) { var componentModel = resultItem.exist; var newCptOption = resultItem.option; assert(isObject(newCptOption) || componentModel, 'Empty component definition'); // Consider where is no new option and should be merged using {}, // see removeEdgeAndAdd in topologicalTravel and // ComponentModel.getAllClassMainTypes. if (!newCptOption) { componentModel.mergeOption({}, this); componentModel.optionUpdated({}, false); } else { var ComponentModelClass = ComponentModel.getClass(mainType, resultItem.keyInfo.subType, true); if (componentModel && componentModel.constructor === ComponentModelClass) { componentModel.name = resultItem.keyInfo.name; // componentModel.settingTask && componentModel.settingTask.dirty(); componentModel.mergeOption(newCptOption, this); componentModel.optionUpdated(newCptOption, false); } else { // PENDING Global as parent ? var extraOpt = extend({ dependentModels: dependentModels, componentIndex: index }, resultItem.keyInfo); componentModel = new ComponentModelClass(newCptOption, this, this, extraOpt); extend(componentModel, extraOpt); componentModel.init(newCptOption, this, this, extraOpt); // Call optionUpdated after init. // newCptOption has been used as componentModel.option // and may be merged with theme and default, so pass null // to avoid confusion. componentModel.optionUpdated(null, true); } } componentsMap.get(mainType)[index] = componentModel; option[mainType][index] = componentModel.option; }, this); // Backup series for filtering. if (mainType === 'series') { createSeriesIndices(this, componentsMap.get('series')); } } this._seriesIndicesMap = createHashMap(this._seriesIndices = this._seriesIndices || []); }, /** * Get option for output (cloned option and inner info removed) * @public * @return {Object} */ getOption: function () { var option = clone(this.option); each(option, function (opts, mainType) { if (ComponentModel.hasClass(mainType)) { var opts = modelUtil.normalizeToArray(opts); for (var i = opts.length - 1; i >= 0; i--) { // Remove options with inner id. if (modelUtil.isIdInner(opts[i])) { opts.splice(i, 1); } } option[mainType] = opts; } }); delete option[OPTION_INNER_KEY]; return option; }, /** * @return {module:echarts/model/Model} */ getTheme: function () { return this._theme; }, /** * @param {string} mainType * @param {number} [idx=0] * @return {module:echarts/model/Component} */ getComponent: function (mainType, idx) { var list = this._componentsMap.get(mainType); if (list) { return list[idx || 0]; } }, /** * If none of index and id and name used, return all components with mainType. * @param {Object} condition * @param {string} condition.mainType * @param {string} [condition.subType] If ignore, only query by mainType * @param {number|Array.} [condition.index] Either input index or id or name. * @param {string|Array.} [condition.id] Either input index or id or name. * @param {string|Array.} [condition.name] Either input index or id or name. * @return {Array.} */ queryComponents: function (condition) { var mainType = condition.mainType; if (!mainType) { return []; } var index = condition.index; var id = condition.id; var name = condition.name; var cpts = this._componentsMap.get(mainType); if (!cpts || !cpts.length) { return []; } var result; if (index != null) { if (!isArray(index)) { index = [index]; } result = filter(map(index, function (idx) { return cpts[idx]; }), function (val) { return !!val; }); } else if (id != null) { var isIdArray = isArray(id); result = filter(cpts, function (cpt) { return isIdArray && indexOf(id, cpt.id) >= 0 || !isIdArray && cpt.id === id; }); } else if (name != null) { var isNameArray = isArray(name); result = filter(cpts, function (cpt) { return isNameArray && indexOf(name, cpt.name) >= 0 || !isNameArray && cpt.name === name; }); } else { // Return all components with mainType result = cpts.slice(); } return filterBySubType(result, condition); }, /** * The interface is different from queryComponents, * which is convenient for inner usage. * * @usage * var result = findComponents( * {mainType: 'dataZoom', query: {dataZoomId: 'abc'}} * ); * var result = findComponents( * {mainType: 'series', subType: 'pie', query: {seriesName: 'uio'}} * ); * var result = findComponents( * {mainType: 'series', * filter: function (model, index) {...}} * ); * // result like [component0, componnet1, ...] * * @param {Object} condition * @param {string} condition.mainType Mandatory. * @param {string} [condition.subType] Optional. * @param {Object} [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 {Function} [condition.filter] parameter: component, return boolean. * @return {Array.} */ findComponents: function (condition) { var query = condition.query; var mainType = condition.mainType; var queryCond = getQueryCond(query); var result = queryCond ? this.queryComponents(queryCond) : this._componentsMap.get(mainType); return doFilter(filterBySubType(result, condition)); function getQueryCond(q) { var indexAttr = mainType + 'Index'; var idAttr = mainType + 'Id'; var nameAttr = mainType + 'Name'; return q && (q[indexAttr] != null || q[idAttr] != null || q[nameAttr] != null) ? { mainType: mainType, // subType will be filtered finally. index: q[indexAttr], id: q[idAttr], name: q[nameAttr] } : null; } function doFilter(res) { return condition.filter ? filter(res, condition.filter) : res; } }, /** * @usage * eachComponent('legend', function (legendModel, index) { * ... * }); * eachComponent(function (componentType, model, index) { * // componentType does not include subType * // (componentType is 'xxx' but not 'xxx.aa') * }); * eachComponent( * {mainType: 'dataZoom', query: {dataZoomId: 'abc'}}, * function (model, index) {...} * ); * eachComponent( * {mainType: 'series', subType: 'pie', query: {seriesName: 'uio'}}, * function (model, index) {...} * ); * * @param {string|Object=} mainType When mainType is object, the definition * is the same as the method 'findComponents'. * @param {Function} cb * @param {*} context */ eachComponent: function (mainType, cb, context) { var componentsMap = this._componentsMap; if (typeof mainType === 'function') { context = cb; cb = mainType; componentsMap.each(function (components, componentType) { each(components, function (component, index) { cb.call(context, componentType, component, index); }); }); } else if (isString(mainType)) { each(componentsMap.get(mainType), cb, context); } else if (isObject(mainType)) { var queryResult = this.findComponents(mainType); each(queryResult, cb, context); } }, /** * @param {string} name * @return {Array.} */ getSeriesByName: function (name) { var series = this._componentsMap.get('series'); return filter(series, function (oneSeries) { return oneSeries.name === name; }); }, /** * @param {number} seriesIndex * @return {module:echarts/model/Series} */ getSeriesByIndex: function (seriesIndex) { return this._componentsMap.get('series')[seriesIndex]; }, /** * Get series list before filtered by type. * FIXME: rename to getRawSeriesByType? * * @param {string} subType * @return {Array.} */ getSeriesByType: function (subType) { var series = this._componentsMap.get('series'); return filter(series, function (oneSeries) { return oneSeries.subType === subType; }); }, /** * @return {Array.} */ getSeries: function () { return this._componentsMap.get('series').slice(); }, /** * @return {number} */ getSeriesCount: function () { return this._componentsMap.get('series').length; }, /** * After filtering, series may be different * frome raw series. * * @param {Function} cb * @param {*} context */ eachSeries: function (cb, context) { assertSeriesInitialized(this); each(this._seriesIndices, function (rawSeriesIndex) { var series = this._componentsMap.get('series')[rawSeriesIndex]; cb.call(context, series, rawSeriesIndex); }, this); }, /** * Iterate raw series before filtered. * * @param {Function} cb * @param {*} context */ eachRawSeries: function (cb, context) { each(this._componentsMap.get('series'), cb, context); }, /** * After filtering, series may be different. * frome raw series. * * @param {string} subType. * @param {Function} cb * @param {*} context */ eachSeriesByType: function (subType, cb, context) { assertSeriesInitialized(this); each(this._seriesIndices, function (rawSeriesIndex) { var series = this._componentsMap.get('series')[rawSeriesIndex]; if (series.subType === subType) { cb.call(context, series, rawSeriesIndex); } }, this); }, /** * Iterate raw series before filtered of given type. * * @parma {string} subType * @param {Function} cb * @param {*} context */ eachRawSeriesByType: function (subType, cb, context) { return each(this.getSeriesByType(subType), cb, context); }, /** * @param {module:echarts/model/Series} seriesModel */ isSeriesFiltered: function (seriesModel) { assertSeriesInitialized(this); return this._seriesIndicesMap.get(seriesModel.componentIndex) == null; }, /** * @return {Array.} */ getCurrentSeriesIndices: function () { return (this._seriesIndices || []).slice(); }, /** * @param {Function} cb * @param {*} context */ filterSeries: function (cb, context) { assertSeriesInitialized(this); var filteredSeries = filter(this._componentsMap.get('series'), cb, context); createSeriesIndices(this, filteredSeries); }, restoreData: function (payload) { var componentsMap = this._componentsMap; createSeriesIndices(this, componentsMap.get('series')); var componentTypes = []; componentsMap.each(function (components, componentType) { componentTypes.push(componentType); }); ComponentModel.topologicalTravel(componentTypes, ComponentModel.getAllClassMainTypes(), function (componentType, dependencies) { each(componentsMap.get(componentType), function (component) { (componentType !== 'series' || !isNotTargetSeries(component, payload)) && component.restoreData(); }); }); } }); function isNotTargetSeries(seriesModel, payload) { if (payload) { var index = payload.seiresIndex; var id = payload.seriesId; var name = payload.seriesName; return index != null && seriesModel.componentIndex !== index || id != null && seriesModel.id !== id || name != null && seriesModel.name !== name; } } /** * @inner */ function mergeTheme(option, theme) { // PENDING // NOT use `colorLayer` in theme if option has `color` var notMergeColorLayer = option.color && !option.colorLayer; each(theme, function (themeItem, name) { if (name === 'colorLayer' && notMergeColorLayer) { return; } // 如果有 component model 则把具体的 merge 逻辑交给该 model 处理 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 initBase(baseOption) { baseOption = baseOption; // Using OPTION_INNER_KEY to mark that this option can not be used outside, // i.e. `chart.setOption(chart.getModel().option);` is forbiden. this.option = {}; this.option[OPTION_INNER_KEY] = 1; /** * Init with series: [], in case of calling findSeries method * before series initialized. * @type {Object.>} * @private */ this._componentsMap = createHashMap({ series: [] }); /** * Mapping between filtered series list and raw series list. * key: filtered series indices, value: raw series indices. * @type {Array.} * @private */ this._seriesIndices; this._seriesIndicesMap; mergeTheme(baseOption, this._theme.option); // TODO Needs clone when merging to the unexisted property merge(baseOption, globalDefault, false); this.mergeOption(baseOption); } /** * @inner * @param {Array.|string} types model types * @return {Object} key: {string} type, value: {Array.} models */ function getComponentsByTypes(componentsMap, types) { if (!isArray(types)) { types = types ? [types] : []; } var ret = {}; each(types, function (type) { ret[type] = (componentsMap.get(type) || []).slice(); }); return ret; } /** * @inner */ function determineSubType(mainType, newCptOption, existComponent) { var subType = newCptOption.type ? newCptOption.type : existComponent ? existComponent.subType // Use determineSubType only when there is no existComponent. : ComponentModel.determineSubType(mainType, newCptOption); // tooltip, markline, markpoint may always has no subType return subType; } /** * @inner */ function createSeriesIndices(ecModel, seriesModels) { ecModel._seriesIndicesMap = createHashMap(ecModel._seriesIndices = map(seriesModels, function (series) { return series.componentIndex; }) || []); } /** * @inner */ function filterBySubType(components, condition) { // Using hasOwnProperty for restrict. Consider // subType is undefined in user payload. return condition.hasOwnProperty('subType') ? filter(components, function (cpt) { return cpt.subType === condition.subType; }) : components; } /** * @inner */ function assertSeriesInitialized(ecModel) {} mixin(GlobalModel, colorPaletteMixin); var _default = GlobalModel; module.exports = _default;