|  | /* | 
|  | * 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 { | 
|  | each, | 
|  | isObject, | 
|  | isArray, | 
|  | createHashMap, | 
|  | HashMap, | 
|  | map, | 
|  | assert, | 
|  | isString, | 
|  | indexOf, | 
|  | isStringSafe, | 
|  | isNumber | 
|  | } from 'zrender/src/core/util'; | 
|  | import env from 'zrender/src/core/env'; | 
|  | import GlobalModel from '../model/Global'; | 
|  | import ComponentModel, {ComponentModelConstructor} from '../model/Component'; | 
|  | import SeriesData from '../data/SeriesData'; | 
|  | import { | 
|  | ComponentOption, | 
|  | ComponentMainType, | 
|  | ComponentSubType, | 
|  | DisplayStateHostOption, | 
|  | OptionDataItem, | 
|  | OptionDataValue, | 
|  | TooltipRenderMode, | 
|  | Payload, | 
|  | OptionId, | 
|  | OptionName, | 
|  | InterpolatableValue | 
|  | } from './types'; | 
|  | import { Dictionary } from 'zrender/src/core/types'; | 
|  | import SeriesModel from '../model/Series'; | 
|  | import CartesianAxisModel from '../coord/cartesian/AxisModel'; | 
|  | import GridModel from '../coord/cartesian/GridModel'; | 
|  | import { isNumeric, getRandomIdBase, getPrecision, round } from './number'; | 
|  | import { warn } from './log'; | 
|  |  | 
|  | function interpolateNumber(p0: number, p1: number, percent: number): number { | 
|  | return (p1 - p0) * percent + p0; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Make the name displayable. But we should | 
|  | * make sure it is not duplicated with user | 
|  | * specified name, so use '\0'; | 
|  | */ | 
|  | const DUMMY_COMPONENT_NAME_PREFIX = 'series\0'; | 
|  |  | 
|  | const INTERNAL_COMPONENT_ID_PREFIX = '\0_ec_\0'; | 
|  |  | 
|  | /** | 
|  | * If value is not array, then translate it to array. | 
|  | * @param  {*} value | 
|  | * @return {Array} [value] or value | 
|  | */ | 
|  | export function normalizeToArray<T>(value?: T | T[]): T[] { | 
|  | return value instanceof Array | 
|  | ? value | 
|  | : value == null | 
|  | ? [] | 
|  | : [value]; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Sync default option between normal and emphasis like `position` and `show` | 
|  | * In case some one will write code like | 
|  | *     label: { | 
|  | *          show: false, | 
|  | *          position: 'outside', | 
|  | *          fontSize: 18 | 
|  | *     }, | 
|  | *     emphasis: { | 
|  | *          label: { show: true } | 
|  | *     } | 
|  | */ | 
|  | export function defaultEmphasis( | 
|  | opt: DisplayStateHostOption, | 
|  | key: string, | 
|  | subOpts: string[] | 
|  | ): void { | 
|  | // Caution: performance sensitive. | 
|  | if (opt) { | 
|  | opt[key] = opt[key] || {}; | 
|  | opt.emphasis = opt.emphasis || {}; | 
|  | opt.emphasis[key] = opt.emphasis[key] || {}; | 
|  |  | 
|  | // Default emphasis option from normal | 
|  | for (let i = 0, len = subOpts.length; i < len; i++) { | 
|  | const subOptName = subOpts[i]; | 
|  | if (!opt.emphasis[key].hasOwnProperty(subOptName) | 
|  | && opt[key].hasOwnProperty(subOptName) | 
|  | ) { | 
|  | opt.emphasis[key][subOptName] = opt[key][subOptName]; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | export const TEXT_STYLE_OPTIONS = [ | 
|  | 'fontStyle', 'fontWeight', 'fontSize', 'fontFamily', | 
|  | 'rich', 'tag', 'color', 'textBorderColor', 'textBorderWidth', | 
|  | 'width', 'height', 'lineHeight', 'align', 'verticalAlign', 'baseline', | 
|  | 'shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY', | 
|  | 'textShadowColor', 'textShadowBlur', 'textShadowOffsetX', 'textShadowOffsetY', | 
|  | 'backgroundColor', 'borderColor', 'borderWidth', 'borderRadius', 'padding' | 
|  | ] as const; | 
|  |  | 
|  | // modelUtil.LABEL_OPTIONS = modelUtil.TEXT_STYLE_OPTIONS.concat([ | 
|  | //     'position', 'offset', 'rotate', 'origin', 'show', 'distance', 'formatter', | 
|  | //     'fontStyle', 'fontWeight', 'fontSize', 'fontFamily', | 
|  | //     // FIXME: deprecated, check and remove it. | 
|  | //     'textStyle' | 
|  | // ]); | 
|  |  | 
|  | /** | 
|  | * The method does not ensure performance. | 
|  | * data could be [12, 2323, {value: 223}, [1221, 23], {value: [2, 23]}] | 
|  | * This helper method retrieves value from data. | 
|  | */ | 
|  | export function getDataItemValue( | 
|  | dataItem: OptionDataItem | 
|  | ): OptionDataValue | OptionDataValue[] { | 
|  | return (isObject(dataItem) && !isArray(dataItem) && !(dataItem instanceof Date)) | 
|  | ? (dataItem as Dictionary<OptionDataValue>).value : dataItem; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * data could be [12, 2323, {value: 223}, [1221, 23], {value: [2, 23]}] | 
|  | * This helper method determine if dataItem has extra option besides value | 
|  | */ | 
|  | export function isDataItemOption(dataItem: OptionDataItem): boolean { | 
|  | return isObject(dataItem) | 
|  | && !(dataItem instanceof Array); | 
|  | // // markLine data can be array | 
|  | // && !(dataItem[0] && isObject(dataItem[0]) && !(dataItem[0] instanceof Array)); | 
|  | } | 
|  |  | 
|  | // Compatible with previous definition: id could be number (but not recommended). | 
|  | // number and string are treated the same when compared. | 
|  | // number id will not be converted to string in option. | 
|  | // number id will be converted to string in component instance id. | 
|  | export interface MappingExistingItem { | 
|  | id?: OptionId; | 
|  | name?: string; | 
|  | }; | 
|  | /** | 
|  | * The array `MappingResult<T>[]` exactly represents the content of the result | 
|  | * components array after merge. | 
|  | * The indices are the same as the `existings`. | 
|  | * Items will not be `null`/`undefined` even if the corresponding `existings` will be removed. | 
|  | */ | 
|  | type MappingResult<T> = MappingResultItem<T>[]; | 
|  | interface MappingResultItem<T extends MappingExistingItem = MappingExistingItem> { | 
|  | // Existing component instance. | 
|  | existing: T; | 
|  | // The mapped new component option. | 
|  | newOption: ComponentOption; | 
|  | // Mark that the new component has nothing to do with any of the old components. | 
|  | // So they won't share view. Also see `__requireNewView`. | 
|  | brandNew: boolean; | 
|  | // keyInfo for new component. | 
|  | // All of them will be assigned to a created component instance. | 
|  | keyInfo: { | 
|  | name: string, | 
|  | id: string, | 
|  | mainType: ComponentMainType, | 
|  | subType: ComponentSubType | 
|  | }; | 
|  | } | 
|  |  | 
|  | type MappingToExistsMode = 'normalMerge' | 'replaceMerge' | 'replaceAll'; | 
|  |  | 
|  | /** | 
|  | * Mapping to existings for merge. | 
|  | * | 
|  | * Mode "normalMege": | 
|  | *     The mapping result (merge result) will keep the order of the existing | 
|  | *     component, rather than the order of new option. Because we should ensure | 
|  | *     some specified index reference (like xAxisIndex) keep work. | 
|  | *     And in most cases, "merge option" is used to update partial option but not | 
|  | *     be expected to change the order. | 
|  | * | 
|  | * Mode "replaceMege": | 
|  | *     (1) Only the id mapped components will be merged. | 
|  | *     (2) Other existing components (except internal components) will be removed. | 
|  | *     (3) Other new options will be used to create new component. | 
|  | *     (4) The index of the existing components will not be modified. | 
|  | *     That means their might be "hole" after the removal. | 
|  | *     The new components are created first at those available index. | 
|  | * | 
|  | * Mode "replaceAll": | 
|  | *     This mode try to support that reproduce an echarts instance from another | 
|  | *     echarts instance (via `getOption`) in some simple cases. | 
|  | *     In this scenario, the `result` index are exactly the consistent with the `newCmptOptions`, | 
|  | *     which ensures the component index referring (like `xAxisIndex: ?`) corrent. That is, | 
|  | *     the "hole" in `newCmptOptions` will also be kept. | 
|  | *     On the contrary, other modes try best to eliminate holes. | 
|  | *     PENDING: This is an experimental mode yet. | 
|  | * | 
|  | * @return See the comment of <MappingResult>. | 
|  | */ | 
|  | export function mappingToExists<T extends MappingExistingItem>( | 
|  | existings: T[], | 
|  | newCmptOptions: ComponentOption[], | 
|  | mode: MappingToExistsMode | 
|  | ): MappingResult<T> { | 
|  |  | 
|  | const isNormalMergeMode = mode === 'normalMerge'; | 
|  | const isReplaceMergeMode = mode === 'replaceMerge'; | 
|  | const isReplaceAllMode = mode === 'replaceAll'; | 
|  | existings = existings || []; | 
|  | newCmptOptions = (newCmptOptions || []).slice(); | 
|  | const existingIdIdxMap = createHashMap<number>(); | 
|  |  | 
|  | // Validate id and name on user input option. | 
|  | each(newCmptOptions, function (cmptOption, index) { | 
|  | if (!isObject<ComponentOption>(cmptOption)) { | 
|  | newCmptOptions[index] = null; | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (__DEV__) { | 
|  | // There is some legacy case that name is set as `false`. | 
|  | // But should work normally rather than throw error. | 
|  | if (cmptOption.id != null && !isValidIdOrName(cmptOption.id)) { | 
|  | warnInvalidateIdOrName(cmptOption.id); | 
|  | } | 
|  | if (cmptOption.name != null && !isValidIdOrName(cmptOption.name)) { | 
|  | warnInvalidateIdOrName(cmptOption.name); | 
|  | } | 
|  | } | 
|  | }); | 
|  |  | 
|  | const result = prepareResult(existings, existingIdIdxMap, mode); | 
|  |  | 
|  | if (isNormalMergeMode || isReplaceMergeMode) { | 
|  | mappingById(result, existings, existingIdIdxMap, newCmptOptions); | 
|  | } | 
|  |  | 
|  | if (isNormalMergeMode) { | 
|  | mappingByName(result, newCmptOptions); | 
|  | } | 
|  |  | 
|  | if (isNormalMergeMode || isReplaceMergeMode) { | 
|  | mappingByIndex(result, newCmptOptions, isReplaceMergeMode); | 
|  | } | 
|  | else if (isReplaceAllMode) { | 
|  | mappingInReplaceAllMode(result, newCmptOptions); | 
|  | } | 
|  |  | 
|  | makeIdAndName(result); | 
|  |  | 
|  | // The array `result` MUST NOT contain elided items, otherwise the | 
|  | // forEach will omit those items and result in incorrect result. | 
|  | return result; | 
|  | } | 
|  |  | 
|  | function prepareResult<T extends MappingExistingItem>( | 
|  | existings: T[], | 
|  | existingIdIdxMap: HashMap<number>, | 
|  | mode: MappingToExistsMode | 
|  | ): MappingResultItem<T>[] { | 
|  | const result: MappingResultItem<T>[] = []; | 
|  |  | 
|  | if (mode === 'replaceAll') { | 
|  | return result; | 
|  | } | 
|  |  | 
|  | // Do not use native `map` to in case that the array `existings` | 
|  | // contains elided items, which will be omitted. | 
|  | for (let index = 0; index < existings.length; index++) { | 
|  | const existing = existings[index]; | 
|  | // Because of replaceMerge, `existing` may be null/undefined. | 
|  | if (existing && existing.id != null) { | 
|  | existingIdIdxMap.set(existing.id, index); | 
|  | } | 
|  | // For non-internal-componnets: | 
|  | //     Mode "normalMerge": all existings kept. | 
|  | //     Mode "replaceMerge": all existing removed unless mapped by id. | 
|  | // For internal-components: | 
|  | //     go with "replaceMerge" approach in both mode. | 
|  | result.push({ | 
|  | existing: (mode === 'replaceMerge' || isComponentIdInternal(existing)) | 
|  | ? null | 
|  | : existing, | 
|  | newOption: null, | 
|  | keyInfo: null, | 
|  | brandNew: null | 
|  | }); | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | function mappingById<T extends MappingExistingItem>( | 
|  | result: MappingResult<T>, | 
|  | existings: T[], | 
|  | existingIdIdxMap: HashMap<number>, | 
|  | newCmptOptions: ComponentOption[] | 
|  | ): void { | 
|  | // Mapping by id if specified. | 
|  | each(newCmptOptions, function (cmptOption, index) { | 
|  | if (!cmptOption || cmptOption.id == null) { | 
|  | return; | 
|  | } | 
|  | const optionId = makeComparableKey(cmptOption.id); | 
|  | const existingIdx = existingIdIdxMap.get(optionId); | 
|  | if (existingIdx != null) { | 
|  | const resultItem = result[existingIdx]; | 
|  | assert( | 
|  | !resultItem.newOption, | 
|  | 'Duplicated option on id "' + optionId + '".' | 
|  | ); | 
|  | resultItem.newOption = cmptOption; | 
|  | // In both mode, if id matched, new option will be merged to | 
|  | // the existings rather than creating new component model. | 
|  | resultItem.existing = existings[existingIdx]; | 
|  | newCmptOptions[index] = null; | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | function mappingByName<T extends MappingExistingItem>( | 
|  | result: MappingResult<T>, | 
|  | newCmptOptions: ComponentOption[] | 
|  | ): void { | 
|  | // Mapping by name if specified. | 
|  | each(newCmptOptions, function (cmptOption, index) { | 
|  | if (!cmptOption || cmptOption.name == null) { | 
|  | return; | 
|  | } | 
|  | for (let i = 0; i < result.length; i++) { | 
|  | const existing = result[i].existing; | 
|  | if (!result[i].newOption // Consider name: two map to one. | 
|  | // Can not match when both ids existing but different. | 
|  | && existing | 
|  | && (existing.id == null || cmptOption.id == null) | 
|  | && !isComponentIdInternal(cmptOption) | 
|  | && !isComponentIdInternal(existing) | 
|  | && keyExistAndEqual('name', existing, cmptOption) | 
|  | ) { | 
|  | result[i].newOption = cmptOption; | 
|  | newCmptOptions[index] = null; | 
|  | return; | 
|  | } | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | function mappingByIndex<T extends MappingExistingItem>( | 
|  | result: MappingResult<T>, | 
|  | newCmptOptions: ComponentOption[], | 
|  | brandNew: boolean | 
|  | ): void { | 
|  | each(newCmptOptions, function (cmptOption) { | 
|  | if (!cmptOption) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Find the first place that not mapped by id and not internal component (consider the "hole"). | 
|  | let resultItem; | 
|  | let nextIdx = 0; | 
|  | while ( | 
|  | // Be `!resultItem` only when `nextIdx >= result.length`. | 
|  | (resultItem = result[nextIdx]) | 
|  | // (1) Existing models that already have id should be able to mapped to. Because | 
|  | // after mapping performed, model will always be assigned with an id if user not given. | 
|  | // After that all models have id. | 
|  | // (2) If new option has id, it can only set to a hole or append to the last. It should | 
|  | // not be merged to the existings with different id. Because id should not be overwritten. | 
|  | // (3) Name can be overwritten, because axis use name as 'show label text'. | 
|  | && ( | 
|  | resultItem.newOption | 
|  | || isComponentIdInternal(resultItem.existing) | 
|  | || ( | 
|  | // In mode "replaceMerge", here no not-mapped-non-internal-existing. | 
|  | resultItem.existing | 
|  | && cmptOption.id != null | 
|  | && !keyExistAndEqual('id', cmptOption, resultItem.existing) | 
|  | ) | 
|  | ) | 
|  | ) { | 
|  | nextIdx++; | 
|  | } | 
|  |  | 
|  | if (resultItem) { | 
|  | resultItem.newOption = cmptOption; | 
|  | resultItem.brandNew = brandNew; | 
|  | } | 
|  | else { | 
|  | result.push({ | 
|  | newOption: cmptOption, | 
|  | brandNew: brandNew, | 
|  | existing: null, | 
|  | keyInfo: null | 
|  | }); | 
|  | } | 
|  | nextIdx++; | 
|  | }); | 
|  | } | 
|  |  | 
|  | function mappingInReplaceAllMode<T extends MappingExistingItem>( | 
|  | result: MappingResult<T>, | 
|  | newCmptOptions: ComponentOption[] | 
|  | ): void { | 
|  | each(newCmptOptions, function (cmptOption) { | 
|  | // The feature "reproduce" requires "hole" will also reproduced | 
|  | // in case that component index referring are broken. | 
|  | result.push({ | 
|  | newOption: cmptOption, | 
|  | brandNew: true, | 
|  | existing: null, | 
|  | keyInfo: null | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Make id and name for mapping result (result of mappingToExists) | 
|  | * into `keyInfo` field. | 
|  | */ | 
|  | function makeIdAndName( | 
|  | mapResult: MappingResult<MappingExistingItem> | 
|  | ): void { | 
|  | // We use this id to hash component models and view instances | 
|  | // in echarts. id can be specified by user, or auto generated. | 
|  |  | 
|  | // The id generation rule ensures new view instance are able | 
|  | // to mapped to old instance when setOption are called in | 
|  | // no-merge mode. So we generate model id by name and plus | 
|  | // type in view id. | 
|  |  | 
|  | // name can be duplicated among components, which is convenient | 
|  | // to specify multi components (like series) by one name. | 
|  |  | 
|  | // Ensure that each id is distinct. | 
|  | const idMap = createHashMap(); | 
|  |  | 
|  | each(mapResult, function (item) { | 
|  | const existing = item.existing; | 
|  | existing && idMap.set(existing.id, item); | 
|  | }); | 
|  |  | 
|  | each(mapResult, function (item) { | 
|  | const opt = item.newOption; | 
|  |  | 
|  | // Force ensure id not duplicated. | 
|  | assert( | 
|  | !opt || opt.id == null || !idMap.get(opt.id) || idMap.get(opt.id) === item, | 
|  | 'id duplicates: ' + (opt && opt.id) | 
|  | ); | 
|  |  | 
|  | opt && opt.id != null && idMap.set(opt.id, item); | 
|  | !item.keyInfo && (item.keyInfo = {} as MappingResultItem['keyInfo']); | 
|  | }); | 
|  |  | 
|  | // Make name and id. | 
|  | each(mapResult, function (item, index) { | 
|  | const existing = item.existing; | 
|  | const opt = item.newOption; | 
|  | const keyInfo = item.keyInfo; | 
|  |  | 
|  | if (!isObject<ComponentOption>(opt)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Name can be overwritten. Consider case: axis.name = '20km'. | 
|  | // But id generated by name will not be changed, which affect | 
|  | // only in that case: setOption with 'not merge mode' and view | 
|  | // instance will be recreated, which can be accepted. | 
|  | keyInfo.name = opt.name != null | 
|  | ? makeComparableKey(opt.name) | 
|  | : existing | 
|  | ? existing.name | 
|  | // Avoid that different series has the same name, | 
|  | // because name may be used like in color pallet. | 
|  | : DUMMY_COMPONENT_NAME_PREFIX + index; | 
|  |  | 
|  | if (existing) { | 
|  | keyInfo.id = makeComparableKey(existing.id); | 
|  | } | 
|  | else if (opt.id != null) { | 
|  | keyInfo.id = makeComparableKey(opt.id); | 
|  | } | 
|  | else { | 
|  | // Consider this situatoin: | 
|  | //  optionA: [{name: 'a'}, {name: 'a'}, {..}] | 
|  | //  optionB [{..}, {name: 'a'}, {name: 'a'}] | 
|  | // Series with the same name between optionA and optionB | 
|  | // should be mapped. | 
|  | let idNum = 0; | 
|  | do { | 
|  | keyInfo.id = '\0' + keyInfo.name + '\0' + idNum++; | 
|  | } | 
|  | while (idMap.get(keyInfo.id)); | 
|  | } | 
|  |  | 
|  | idMap.set(keyInfo.id, item); | 
|  | }); | 
|  | } | 
|  |  | 
|  | function keyExistAndEqual( | 
|  | attr: 'id' | 'name', | 
|  | obj1: { id?: OptionId, name?: OptionName }, | 
|  | obj2: { id?: OptionId, name?: OptionName } | 
|  | ): boolean { | 
|  | const key1 = convertOptionIdName(obj1[attr], null); | 
|  | const key2 = convertOptionIdName(obj2[attr], null); | 
|  | // See `MappingExistingItem`. `id` and `name` trade string equals to number. | 
|  | return key1 != null && key2 != null && key1 === key2; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return return null if not exist. | 
|  | */ | 
|  | function makeComparableKey(val: unknown): string { | 
|  | if (__DEV__) { | 
|  | if (val == null) { | 
|  | throw new Error(); | 
|  | } | 
|  | } | 
|  | return convertOptionIdName(val, ''); | 
|  | } | 
|  |  | 
|  | export function convertOptionIdName(idOrName: unknown, defaultValue: string): string { | 
|  | if (idOrName == null) { | 
|  | return defaultValue; | 
|  | } | 
|  | return isString(idOrName) | 
|  | ? idOrName | 
|  | : (isNumber(idOrName) || isStringSafe(idOrName)) | 
|  | ? idOrName + '' | 
|  | : defaultValue; | 
|  | } | 
|  |  | 
|  | function warnInvalidateIdOrName(idOrName: unknown) { | 
|  | if (__DEV__) { | 
|  | warn('`' + idOrName + '` is invalid id or name. Must be a string or number.'); | 
|  | } | 
|  | } | 
|  |  | 
|  | function isValidIdOrName(idOrName: unknown): boolean { | 
|  | return isStringSafe(idOrName) || isNumeric(idOrName); | 
|  | } | 
|  |  | 
|  | export function isNameSpecified(componentModel: ComponentModel): boolean { | 
|  | const name = componentModel.name; | 
|  | // Is specified when `indexOf` get -1 or > 0. | 
|  | return !!(name && name.indexOf(DUMMY_COMPONENT_NAME_PREFIX)); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @public | 
|  | * @param {Object} cmptOption | 
|  | * @return {boolean} | 
|  | */ | 
|  | export function isComponentIdInternal(cmptOption: { id?: MappingExistingItem['id'] }): boolean { | 
|  | return cmptOption | 
|  | && cmptOption.id != null | 
|  | && makeComparableKey(cmptOption.id).indexOf(INTERNAL_COMPONENT_ID_PREFIX) === 0; | 
|  | } | 
|  |  | 
|  | export function makeInternalComponentId(idSuffix: string) { | 
|  | return INTERNAL_COMPONENT_ID_PREFIX + idSuffix; | 
|  | } | 
|  |  | 
|  | export function setComponentTypeToKeyInfo( | 
|  | mappingResult: MappingResult<MappingExistingItem & { subType?: ComponentSubType }>, | 
|  | mainType: ComponentMainType, | 
|  | componentModelCtor: ComponentModelConstructor | 
|  | ): void { | 
|  | // Set mainType and complete subType. | 
|  | each(mappingResult, function (item) { | 
|  | const newOption = item.newOption; | 
|  | if (isObject(newOption)) { | 
|  | item.keyInfo.mainType = mainType; | 
|  | item.keyInfo.subType = determineSubType(mainType, newOption, item.existing, componentModelCtor); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | function determineSubType( | 
|  | mainType: ComponentMainType, | 
|  | newCmptOption: ComponentOption, | 
|  | existComponent: { subType?: ComponentSubType }, | 
|  | componentModelCtor: ComponentModelConstructor | 
|  | ): ComponentSubType { | 
|  | const subType = newCmptOption.type | 
|  | ? newCmptOption.type | 
|  | : existComponent | 
|  | ? existComponent.subType | 
|  | // Use determineSubType only when there is no existComponent. | 
|  | : (componentModelCtor as ComponentModelConstructor).determineSubType(mainType, newCmptOption); | 
|  |  | 
|  | // tooltip, markline, markpoint may always has no subType | 
|  | return subType; | 
|  | } | 
|  |  | 
|  |  | 
|  | type BatchItem = { | 
|  | seriesId: OptionId, | 
|  | dataIndex: number | number[] | 
|  | }; | 
|  | /** | 
|  | * A helper for removing duplicate items between batchA and batchB, | 
|  | * and in themselves, and categorize by series. | 
|  | * | 
|  | * @param batchA Like: [{seriesId: 2, dataIndex: [32, 4, 5]}, ...] | 
|  | * @param batchB Like: [{seriesId: 2, dataIndex: [32, 4, 5]}, ...] | 
|  | * @return result: [resultBatchA, resultBatchB] | 
|  | */ | 
|  | export function compressBatches( | 
|  | batchA: BatchItem[], | 
|  | batchB: BatchItem[] | 
|  | ): [BatchItem[], BatchItem[]] { | 
|  |  | 
|  | type InnerMap = { | 
|  | [seriesId: string]: { | 
|  | [dataIndex: string]: 1 | 
|  | } | 
|  | }; | 
|  | const mapA = {} as InnerMap; | 
|  | const mapB = {} as InnerMap; | 
|  |  | 
|  | makeMap(batchA || [], mapA); | 
|  | makeMap(batchB || [], mapB, mapA); | 
|  |  | 
|  | return [mapToArray(mapA), mapToArray(mapB)]; | 
|  |  | 
|  | function makeMap(sourceBatch: BatchItem[], map: InnerMap, otherMap?: InnerMap): void { | 
|  | for (let i = 0, len = sourceBatch.length; i < len; i++) { | 
|  | const seriesId = convertOptionIdName(sourceBatch[i].seriesId, null); | 
|  | if (seriesId == null) { | 
|  | return; | 
|  | } | 
|  | const dataIndices = normalizeToArray(sourceBatch[i].dataIndex); | 
|  | const otherDataIndices = otherMap && otherMap[seriesId]; | 
|  |  | 
|  | for (let j = 0, lenj = dataIndices.length; j < lenj; j++) { | 
|  | const dataIndex = dataIndices[j]; | 
|  |  | 
|  | if (otherDataIndices && otherDataIndices[dataIndex]) { | 
|  | otherDataIndices[dataIndex] = null; | 
|  | } | 
|  | else { | 
|  | (map[seriesId] || (map[seriesId] = {}))[dataIndex] = 1; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | function mapToArray(map: Dictionary<any>, isData?: boolean): any[] { | 
|  | const result = []; | 
|  | for (const i in map) { | 
|  | if (map.hasOwnProperty(i) && map[i] != null) { | 
|  | if (isData) { | 
|  | result.push(+i); | 
|  | } | 
|  | else { | 
|  | const dataIndices = mapToArray(map[i], true); | 
|  | dataIndices.length && result.push({seriesId: i, dataIndex: dataIndices}); | 
|  | } | 
|  | } | 
|  | } | 
|  | return result; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param payload Contains dataIndex (means rawIndex) / dataIndexInside / name | 
|  | *                         each of which can be Array or primary type. | 
|  | * @return dataIndex If not found, return undefined/null. | 
|  | */ | 
|  | export function queryDataIndex(data: SeriesData, payload: Payload & { | 
|  | dataIndexInside?: number | number[] | 
|  | dataIndex?: number | number[] | 
|  | name?: string | string[] | 
|  | }): number | number[] { | 
|  | if (payload.dataIndexInside != null) { | 
|  | return payload.dataIndexInside; | 
|  | } | 
|  | else if (payload.dataIndex != null) { | 
|  | return isArray(payload.dataIndex) | 
|  | ? map(payload.dataIndex, function (value) { | 
|  | return data.indexOfRawIndex(value); | 
|  | }) | 
|  | : data.indexOfRawIndex(payload.dataIndex); | 
|  | } | 
|  | else if (payload.name != null) { | 
|  | return isArray(payload.name) | 
|  | ? map(payload.name, function (value) { | 
|  | return data.indexOfName(value); | 
|  | }) | 
|  | : data.indexOfName(payload.name); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Enable property storage to any host object. | 
|  | * Notice: Serialization is not supported. | 
|  | * | 
|  | * For example: | 
|  | * let inner = zrUitl.makeInner(); | 
|  | * | 
|  | * function some1(hostObj) { | 
|  | *      inner(hostObj).someProperty = 1212; | 
|  | *      ... | 
|  | * } | 
|  | * function some2() { | 
|  | *      let fields = inner(this); | 
|  | *      fields.someProperty1 = 1212; | 
|  | *      fields.someProperty2 = 'xx'; | 
|  | *      ... | 
|  | * } | 
|  | * | 
|  | * @return {Function} | 
|  | */ | 
|  | export function makeInner<T, Host extends object>() { | 
|  | const key = '__ec_inner_' + innerUniqueIndex++; | 
|  | return function (hostObj: Host): T { | 
|  | return (hostObj as any)[key] || ((hostObj as any)[key] = {}); | 
|  | }; | 
|  | } | 
|  | let innerUniqueIndex = getRandomIdBase(); | 
|  |  | 
|  | /** | 
|  | * If string, e.g., 'geo', means {geoIndex: 0}. | 
|  | * If Object, could contain some of these properties below: | 
|  | * { | 
|  | *     seriesIndex, seriesId, seriesName, | 
|  | *     geoIndex, geoId, geoName, | 
|  | *     bmapIndex, bmapId, bmapName, | 
|  | *     xAxisIndex, xAxisId, xAxisName, | 
|  | *     yAxisIndex, yAxisId, yAxisName, | 
|  | *     gridIndex, gridId, gridName, | 
|  | *     ... (can be extended) | 
|  | * } | 
|  | * Each properties can be number|string|Array.<number>|Array.<string> | 
|  | * For example, a finder could be | 
|  | * { | 
|  | *     seriesIndex: 3, | 
|  | *     geoId: ['aa', 'cc'], | 
|  | *     gridName: ['xx', 'rr'] | 
|  | * } | 
|  | * xxxIndex can be set as 'all' (means all xxx) or 'none' (means not specify) | 
|  | * If nothing or null/undefined specified, return nothing. | 
|  | * If both `abcIndex`, `abcId`, `abcName` specified, only one work. | 
|  | * The priority is: index > id > name, the same with `ecModel.queryComponents`. | 
|  | */ | 
|  | export type ModelFinderIndexQuery = number | number[] | 'all' | 'none' | false; | 
|  | export type ModelFinderIdQuery = OptionId | OptionId[]; | 
|  | export type ModelFinderNameQuery = OptionId | OptionId[]; | 
|  | // If string, like 'series', means { seriesIndex: 0 }. | 
|  | export type ModelFinder = string | ModelFinderObject; | 
|  | export type ModelFinderObject = { | 
|  | seriesIndex?: ModelFinderIndexQuery, seriesId?: ModelFinderIdQuery, seriesName?: ModelFinderNameQuery | 
|  | geoIndex?: ModelFinderIndexQuery, geoId?: ModelFinderIdQuery, geoName?: ModelFinderNameQuery | 
|  | bmapIndex?: ModelFinderIndexQuery, bmapId?: ModelFinderIdQuery, bmapName?: ModelFinderNameQuery | 
|  | xAxisIndex?: ModelFinderIndexQuery, xAxisId?: ModelFinderIdQuery, xAxisName?: ModelFinderNameQuery | 
|  | yAxisIndex?: ModelFinderIndexQuery, yAxisId?: ModelFinderIdQuery, yAxisName?: ModelFinderNameQuery | 
|  | gridIndex?: ModelFinderIndexQuery, gridId?: ModelFinderIdQuery, gridName?: ModelFinderNameQuery | 
|  | dataIndex?: number, dataIndexInside?: number | 
|  | // ... (can be extended) | 
|  | }; | 
|  | /** | 
|  | * { | 
|  | *     seriesModels: [seriesModel1, seriesModel2], | 
|  | *     seriesModel: seriesModel1, // The first model | 
|  | *     geoModels: [geoModel1, geoModel2], | 
|  | *     geoModel: geoModel1, // The first model | 
|  | *     ... | 
|  | * } | 
|  | */ | 
|  | export type ParsedModelFinder = { | 
|  | // other components | 
|  | [key: string]: ComponentModel | ComponentModel[] | undefined; | 
|  | }; | 
|  |  | 
|  | export type ParsedModelFinderKnown = ParsedModelFinder & { | 
|  | seriesModels?: SeriesModel[]; | 
|  | seriesModel?: SeriesModel; | 
|  | xAxisModels?: CartesianAxisModel[]; | 
|  | xAxisModel?: CartesianAxisModel; | 
|  | yAxisModels?: CartesianAxisModel[]; | 
|  | yAxisModel?: CartesianAxisModel; | 
|  | gridModels?: GridModel[]; | 
|  | gridModel?: GridModel; | 
|  | dataIndex?: number; | 
|  | dataIndexInside?: number; | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * The same behavior as `component.getReferringComponents`. | 
|  | */ | 
|  | export function parseFinder( | 
|  | ecModel: GlobalModel, | 
|  | finderInput: ModelFinder, | 
|  | opt?: { | 
|  | // If no main type specified, use this main type. | 
|  | defaultMainType?: ComponentMainType; | 
|  | // If pervided, types out of this list will be ignored. | 
|  | includeMainTypes?: ComponentMainType[]; | 
|  | enableAll?: boolean; | 
|  | enableNone?: boolean; | 
|  | } | 
|  | ): ParsedModelFinder { | 
|  | const { mainTypeSpecified, queryOptionMap, others } = preParseFinder(finderInput, opt); | 
|  | const result = others as ParsedModelFinderKnown; | 
|  |  | 
|  | const defaultMainType = opt ? opt.defaultMainType : null; | 
|  | if (!mainTypeSpecified && defaultMainType) { | 
|  | queryOptionMap.set(defaultMainType, {}); | 
|  | } | 
|  |  | 
|  | queryOptionMap.each(function (queryOption, mainType) { | 
|  | const queryResult = queryReferringComponents( | 
|  | ecModel, | 
|  | mainType, | 
|  | queryOption, | 
|  | { | 
|  | useDefault: defaultMainType === mainType, | 
|  | enableAll: (opt && opt.enableAll != null) ? opt.enableAll : true, | 
|  | enableNone: (opt && opt.enableNone != null) ? opt.enableNone : true | 
|  | } | 
|  | ); | 
|  | result[mainType + 'Models'] = queryResult.models; | 
|  | result[mainType + 'Model'] = queryResult.models[0]; | 
|  | }); | 
|  |  | 
|  | return result; | 
|  | } | 
|  |  | 
|  | export function preParseFinder( | 
|  | finderInput: ModelFinder, | 
|  | opt?: { | 
|  | // If pervided, types out of this list will be ignored. | 
|  | includeMainTypes?: ComponentMainType[]; | 
|  | } | 
|  | ): { | 
|  | mainTypeSpecified: boolean; | 
|  | queryOptionMap: HashMap<QueryReferringUserOption, ComponentMainType>; | 
|  | others: Partial<Pick<ParsedModelFinderKnown, 'dataIndex' | 'dataIndexInside'>> | 
|  | } { | 
|  | let finder: ModelFinderObject; | 
|  | if (isString(finderInput)) { | 
|  | const obj = {}; | 
|  | (obj as any)[finderInput + 'Index'] = 0; | 
|  | finder = obj; | 
|  | } | 
|  | else { | 
|  | finder = finderInput; | 
|  | } | 
|  |  | 
|  | const queryOptionMap = createHashMap<QueryReferringUserOption, ComponentMainType>(); | 
|  | const others = {} as Partial<Pick<ParsedModelFinderKnown, 'dataIndex' | 'dataIndexInside'>>; | 
|  | let mainTypeSpecified = false; | 
|  |  | 
|  | each(finder, function (value, key) { | 
|  | // Exclude 'dataIndex' and other illgal keys. | 
|  | if (key === 'dataIndex' || key === 'dataIndexInside') { | 
|  | others[key] = value as number; | 
|  | return; | 
|  | } | 
|  |  | 
|  | const parsedKey = key.match(/^(\w+)(Index|Id|Name)$/) || []; | 
|  | const mainType = parsedKey[1]; | 
|  | const queryType = (parsedKey[2] || '').toLowerCase() as keyof QueryReferringUserOption; | 
|  |  | 
|  | if ( | 
|  | !mainType | 
|  | || !queryType | 
|  | || (opt && opt.includeMainTypes && indexOf(opt.includeMainTypes, mainType) < 0) | 
|  | ) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | mainTypeSpecified = mainTypeSpecified || !!mainType; | 
|  |  | 
|  | const queryOption = queryOptionMap.get(mainType) || queryOptionMap.set(mainType, {}); | 
|  | queryOption[queryType] = value as any; | 
|  | }); | 
|  |  | 
|  | return { mainTypeSpecified, queryOptionMap, others }; | 
|  | } | 
|  |  | 
|  |  | 
|  | export type QueryReferringUserOption = { | 
|  | index?: ModelFinderIndexQuery, | 
|  | id?: ModelFinderIdQuery, | 
|  | name?: ModelFinderNameQuery, | 
|  | }; | 
|  |  | 
|  | export const SINGLE_REFERRING: QueryReferringOpt = { useDefault: true, enableAll: false, enableNone: false }; | 
|  | export const MULTIPLE_REFERRING: QueryReferringOpt = { useDefault: false, enableAll: true, enableNone: true }; | 
|  |  | 
|  | export type QueryReferringOpt = { | 
|  | // Whether to use the first component as the default if none of index/id/name are specified. | 
|  | useDefault?: boolean; | 
|  | // Whether to enable `'all'` on index option. | 
|  | enableAll?: boolean; | 
|  | // Whether to enable `'none'`/`false` on index option. | 
|  | enableNone?: boolean; | 
|  | }; | 
|  |  | 
|  | export function queryReferringComponents( | 
|  | ecModel: GlobalModel, | 
|  | mainType: ComponentMainType, | 
|  | userOption: QueryReferringUserOption, | 
|  | opt?: QueryReferringOpt | 
|  | ): { | 
|  | // Always be array rather than null/undefined, which is convenient to use. | 
|  | models: ComponentModel[]; | 
|  | // Whether there is indexOption/id/name specified | 
|  | specified: boolean; | 
|  | } { | 
|  | opt = opt || SINGLE_REFERRING as QueryReferringOpt; | 
|  | let indexOption = userOption.index; | 
|  | let idOption = userOption.id; | 
|  | let nameOption = userOption.name; | 
|  |  | 
|  | const result = { | 
|  | models: null as ComponentModel[], | 
|  | specified: indexOption != null || idOption != null || nameOption != null | 
|  | }; | 
|  |  | 
|  | if (!result.specified) { | 
|  | // Use the first as default if `useDefault`. | 
|  | let firstCmpt; | 
|  | result.models = ( | 
|  | opt.useDefault && (firstCmpt = ecModel.getComponent(mainType)) | 
|  | ) ? [firstCmpt] : []; | 
|  | return result; | 
|  | } | 
|  |  | 
|  | if (indexOption === 'none' || indexOption === false) { | 
|  | assert(opt.enableNone, '`"none"` or `false` is not a valid value on index option.'); | 
|  | result.models = []; | 
|  | return result; | 
|  | } | 
|  |  | 
|  | // `queryComponents` will return all components if | 
|  | // both all of index/id/name are null/undefined. | 
|  | if (indexOption === 'all') { | 
|  | assert(opt.enableAll, '`"all"` is not a valid value on index option.'); | 
|  | indexOption = idOption = nameOption = null; | 
|  | } | 
|  | result.models = ecModel.queryComponents({ | 
|  | mainType: mainType, | 
|  | index: indexOption as number | number[], | 
|  | id: idOption, | 
|  | name: nameOption | 
|  | }); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | export function setAttribute(dom: HTMLElement, key: string, value: any) { | 
|  | dom.setAttribute | 
|  | ? dom.setAttribute(key, value) | 
|  | : ((dom as any)[key] = value); | 
|  | } | 
|  |  | 
|  | export function getAttribute(dom: HTMLElement, key: string): any { | 
|  | return dom.getAttribute | 
|  | ? dom.getAttribute(key) | 
|  | : (dom as any)[key]; | 
|  | } | 
|  |  | 
|  | export function getTooltipRenderMode(renderModeOption: TooltipRenderMode | 'auto'): TooltipRenderMode { | 
|  | if (renderModeOption === 'auto') { | 
|  | // Using html when `document` exists, use richText otherwise | 
|  | return env.domSupported ? 'html' : 'richText'; | 
|  | } | 
|  | else { | 
|  | return renderModeOption || 'html'; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Group a list by key. | 
|  | */ | 
|  | export function groupData<T, R extends string | number>( | 
|  | array: T[], | 
|  | getKey: (item: T) => R // return key | 
|  | ): { | 
|  | keys: R[], | 
|  | buckets: HashMap<T[], R> // hasmap key: the key returned by `getKey`. | 
|  | } { | 
|  | const buckets = createHashMap<T[], R>(); | 
|  | const keys: R[] = []; | 
|  |  | 
|  | each(array, function (item) { | 
|  | const key = getKey(item); | 
|  | (buckets.get(key) | 
|  | || (keys.push(key), buckets.set(key, [])) | 
|  | ).push(item); | 
|  | }); | 
|  |  | 
|  | return { | 
|  | keys: keys, | 
|  | buckets: buckets | 
|  | }; | 
|  | } | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Interpolate raw values of a series with percent | 
|  | * | 
|  | * @param data         data | 
|  | * @param labelModel   label model of the text element | 
|  | * @param sourceValue  start value. May be null/undefined when init. | 
|  | * @param targetValue  end value | 
|  | * @param percent      0~1 percentage; 0 uses start value while 1 uses end value | 
|  | * @return             interpolated values | 
|  | *                     If `sourceValue` and `targetValue` are `number`, return `number`. | 
|  | *                     If `sourceValue` and `targetValue` are `string`, return `string`. | 
|  | *                     If `sourceValue` and `targetValue` are `(string | number)[]`, return `(string | number)[]`. | 
|  | *                     Other cases do not supported. | 
|  | */ | 
|  | export function interpolateRawValues( | 
|  | data: SeriesData, | 
|  | precision: number | 'auto', | 
|  | sourceValue: InterpolatableValue, | 
|  | targetValue: InterpolatableValue, | 
|  | percent: number | 
|  | ): InterpolatableValue { | 
|  | const isAutoPrecision = precision == null || precision === 'auto'; | 
|  |  | 
|  | if (targetValue == null) { | 
|  | return targetValue; | 
|  | } | 
|  |  | 
|  | if (isNumber(targetValue)) { | 
|  | const value = interpolateNumber( | 
|  | sourceValue as number || 0, | 
|  | targetValue as number, | 
|  | percent | 
|  | ); | 
|  | return round( | 
|  | value, | 
|  | isAutoPrecision ? Math.max( | 
|  | getPrecision(sourceValue as number || 0), | 
|  | getPrecision(targetValue as number) | 
|  | ) | 
|  | : precision as number | 
|  | ); | 
|  | } | 
|  | else if (isString(targetValue)) { | 
|  | return percent < 1 ? sourceValue : targetValue; | 
|  | } | 
|  | else { | 
|  | const interpolated = []; | 
|  | const leftArr = sourceValue as (string | number)[]; | 
|  | const rightArr = targetValue as (string | number[]); | 
|  | const length = Math.max(leftArr ? leftArr.length : 0, rightArr.length); | 
|  | for (let i = 0; i < length; ++i) { | 
|  | const info = data.getDimensionInfo(i); | 
|  | // Don't interpolate ordinal dims | 
|  | if (info && info.type === 'ordinal') { | 
|  | // In init, there is no `sourceValue`, but should better not to get undefined result. | 
|  | interpolated[i] = (percent < 1 && leftArr ? leftArr : rightArr)[i] as number; | 
|  | } | 
|  | else { | 
|  | const leftVal = leftArr && leftArr[i] ? leftArr[i] as number : 0; | 
|  | const rightVal = rightArr[i] as number; | 
|  | const value = interpolateNumber(leftVal, rightVal, percent); | 
|  | interpolated[i] = round( | 
|  | value, | 
|  | isAutoPrecision ? Math.max( | 
|  | getPrecision(leftVal), | 
|  | getPrecision(rightVal) | 
|  | ) | 
|  | : precision as number | 
|  | ); | 
|  | } | 
|  | } | 
|  | return interpolated; | 
|  | } | 
|  | } |