/* * 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(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).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[]` 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 = MappingResultItem[]; interface MappingResultItem { // 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 . */ export function mappingToExists( existings: T[], newCmptOptions: ComponentOption[], mode: MappingToExistsMode ): MappingResult { const isNormalMergeMode = mode === 'normalMerge'; const isReplaceMergeMode = mode === 'replaceMerge'; const isReplaceAllMode = mode === 'replaceAll'; existings = existings || []; newCmptOptions = (newCmptOptions || []).slice(); const existingIdIdxMap = createHashMap(); // Validate id and name on user input option. each(newCmptOptions, function (cmptOption, index) { if (!isObject(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( existings: T[], existingIdIdxMap: HashMap, mode: MappingToExistsMode ): MappingResultItem[] { const result: MappingResultItem[] = []; 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( result: MappingResult, existings: T[], existingIdIdxMap: HashMap, 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( result: MappingResult, 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( result: MappingResult, 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( result: MappingResult, 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 ): 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(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, 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, 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() { 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.|Array. * 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; others: Partial> } { let finder: ModelFinderObject; if (isString(finderInput)) { const obj = {}; (obj as any)[finderInput + 'Index'] = 0; finder = obj; } else { finder = finderInput; } const queryOptionMap = createHashMap(); const others = {} as Partial>; 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( array: T[], getKey: (item: T) => R // return key ): { keys: R[], buckets: HashMap // hasmap key: the key returned by `getKey`. } { const buckets = createHashMap(); 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; } }