/* * 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. */ /* global Int32Array */ import * as zrUtil from 'zrender/src/core/util'; import {PathStyleProps} from 'zrender/src/graphic/Path'; import Model from '../model/Model'; import DataDiffer from './DataDiffer'; import {DataProvider, DefaultDataProvider} from './helper/dataProvider'; import {summarizeDimensions, DimensionSummary} from './helper/dimensionHelper'; import SeriesDimensionDefine from './SeriesDimensionDefine'; import {ArrayLike, Dictionary, FunctionPropertyNames} from 'zrender/src/core/types'; import Element from 'zrender/src/Element'; import { DimensionIndex, DimensionName, DimensionLoose, OptionDataItem, ParsedValue, ParsedValueNumeric, ModelOption, SeriesDataType, OptionSourceData, SOURCE_FORMAT_TYPED_ARRAY, SOURCE_FORMAT_ORIGINAL, DecalObject, OrdinalNumber, OrdinalRawValue } from '../util/types'; import {convertOptionIdName, isDataItemOption} from '../util/model'; import { setCommonECData } from '../util/innerStore'; import type Graph from './Graph'; import type Tree from './Tree'; import type { VisualMeta } from '../component/visualMap/VisualMapModel'; import {isSourceInstance, Source} from './Source'; import { LineStyleProps } from '../model/mixin/lineStyle'; import DataStore, { DataStoreDimensionDefine, DimValueGetter } from './DataStore'; import { isSeriesDataSchema, SeriesDataSchema } from './helper/SeriesDataSchema'; const isObject = zrUtil.isObject; const map = zrUtil.map; const CtorInt32Array = typeof Int32Array === 'undefined' ? Array : Int32Array; // Use prefix to avoid index to be the same as otherIdList[idx], // which will cause weird update animation. const ID_PREFIX = 'e\0\0'; const INDEX_NOT_FOUND = -1; type NameRepeatCount = {[name: string]: number}; type ItrParamDims = DimensionLoose | Array; // If Ctx not specified, use List as Ctx type CtxOrList = unknown extends Ctx ? SeriesData : Ctx; type EachCb0 = (this: CtxOrList, idx: number) => void; type EachCb1 = (this: CtxOrList, x: ParsedValue, idx: number) => void; type EachCb2 = (this: CtxOrList, x: ParsedValue, y: ParsedValue, idx: number) => void; type EachCb = (this: CtxOrList, ...args: any) => void; type FilterCb0 = (this: CtxOrList, idx: number) => boolean; type FilterCb1 = (this: CtxOrList, x: ParsedValue, idx: number) => boolean; type FilterCb2 = (this: CtxOrList, x: ParsedValue, y: ParsedValue, idx: number) => boolean; type FilterCb = (this: CtxOrList, ...args: any) => boolean; type MapArrayCb0 = (this: CtxOrList, idx: number) => any; type MapArrayCb1 = (this: CtxOrList, x: ParsedValue, idx: number) => any; type MapArrayCb2 = (this: CtxOrList, x: ParsedValue, y: ParsedValue, idx: number) => any; type MapArrayCb = (this: CtxOrList, ...args: any) => any; type MapCb1 = (this: CtxOrList, x: ParsedValue, idx: number) => ParsedValue | ParsedValue[]; type MapCb2 = (this: CtxOrList, x: ParsedValue, y: ParsedValue, idx: number) => ParsedValue | ParsedValue[]; type MapCb = (this: CtxOrList, ...args: any) => ParsedValue | ParsedValue[]; type SeriesDimensionDefineLoose = string | object | SeriesDimensionDefine; // `SeriesDimensionLoose` and `SeriesDimensionName` is the dimension that is used by coordinate // system or declared in `series.encode`, which will be saved in `SeriesData`. Other dimension // might not be saved in `SeriesData` for performance consideration. See `createDimension` for // more details. type SeriesDimensionLoose = DimensionLoose; type SeriesDimensionName = DimensionName; // type SeriesDimensionIndex = DimensionIndex; const TRANSFERABLE_PROPERTIES = [ 'hasItemOption', '_nameList', '_idList', '_invertedIndicesMap', '_dimSummary', 'userOutput', '_rawData', '_dimValueGetter', '_nameDimIdx', '_idDimIdx', '_nameRepeatCount' ]; const CLONE_PROPERTIES = [ '_approximateExtent' ]; export interface DefaultDataVisual { style: PathStyleProps // Draw type determined which prop should be set with encoded color. // It's only available on the global visual. Use getVisual('drawType') to access it. // It will be set in visual/style.ts module in the first priority. drawType: 'fill' | 'stroke' symbol?: string symbolSize?: number | number[] symbolRotate?: number symbolKeepAspect?: boolean symbolOffset?: string | number | (string | number)[] liftZ?: number // For legend. legendIcon?: string legendLineStyle?: LineStyleProps // visualMap will inject visualMeta data visualMeta?: VisualMeta[] // If color is encoded from palette colorFromPalette?: boolean decal?: DecalObject } export interface DataCalculationInfo { stackedDimension: DimensionName; stackedByDimension: DimensionName; isStackedByIndex: boolean; stackedOverDimension: DimensionName; stackResultDimension: DimensionName; stackedOnSeries?: SERIES_MODEL; } // ----------------------------- // Internal method declarations: // ----------------------------- let prepareInvertedIndex: (data: SeriesData) => void; let getId: (data: SeriesData, rawIndex: number) => string; let getIdNameFromStore: (data: SeriesData, dimIdx: number, dataIdx: number) => string; let normalizeDimensions: (dimensions: ItrParamDims) => Array; let transferProperties: (target: SeriesData, source: SeriesData) => void; let cloneListForMapAndSample: (original: SeriesData) => SeriesData; let makeIdFromName: (data: SeriesData, idx: number) => void; class SeriesData< HostModel extends Model = Model, Visual extends DefaultDataVisual = DefaultDataVisual > { readonly type = 'list'; /** * Name of dimensions list of SeriesData. * * @caution Carefully use the index of this array. * Because when DataStore is an extra high dimension(>30) dataset. We will only pick * the used dimensions from DataStore to avoid performance issue. */ readonly dimensions: SeriesDimensionName[]; // Information of each data dimension, like data type. private _dimInfos: Record; private _dimOmitted = false; private _schema?: SeriesDataSchema; /** * @pending * Actually we do not really need to convert dimensionIndex to dimensionName * and do not need `_dimIdxToName` if we do everything internally based on dimension * index rather than dimension name. */ private _dimIdxToName?: zrUtil.HashMap; readonly hostModel: HostModel; /** * @readonly */ dataType: SeriesDataType; /** * @readonly * Host graph if List is used to store graph nodes / edges. */ graph?: Graph; /** * @readonly * Host tree if List is used to store tree nodes. */ tree?: Tree; private _store: DataStore; private _nameList: string[] = []; private _idList: string[] = []; // Models of data option is stored sparse for optimizing memory cost // Never used yet (not used yet). // private _optionModels: Model[] = []; // Global visual properties after visual coding private _visual: Dictionary = {}; // Global layout properties. private _layout: Dictionary = {}; // Item visual properties after visual coding private _itemVisuals: Dictionary[] = []; // Item layout properties after layout private _itemLayouts: any[] = []; // Graphic elements private _graphicEls: Element[] = []; // key: dim, value: extent private _approximateExtent: Record = {}; private _dimSummary: DimensionSummary; private _invertedIndicesMap: Record>; private _calculationInfo: DataCalculationInfo = {} as DataCalculationInfo; // User output info of this data. // DO NOT use it in other places! // When preparing user params for user callbacks, we have // to clone these inner data structures to prevent users // from modifying them to effect built-in logic. And for // performance consideration we make this `userOutput` to // avoid clone them too many times. userOutput: DimensionSummary['userOutput']; // Having detected that there is data item is non primitive type // (in type `OptionDataItemObject`). // Like `data: [ { value: xx, itemStyle: {...} }, ...]` // At present it only happen in `SOURCE_FORMAT_ORIGINAL`. hasItemOption: boolean = false; // id or name is used on dynamic data, mapping old and new items. // When generating id from name, avoid repeat. private _nameRepeatCount: NameRepeatCount; private _nameDimIdx: number; private _idDimIdx: number; private __wrappedMethods: string[]; // Methods that create a new list based on this list should be listed here. // Notice that those method should `RETURN` the new list. TRANSFERABLE_METHODS = ['cloneShallow', 'downSample', 'lttbDownSample', 'map'] as const; // Methods that change indices of this list should be listed here. CHANGABLE_METHODS = ['filterSelf', 'selectRange'] as const; DOWNSAMPLE_METHODS = ['downSample', 'lttbDownSample'] as const; /** * @param dimensionsInput.dimensions * For example, ['someDimName', {name: 'someDimName', type: 'someDimType'}, ...]. * Dimensions should be concrete names like x, y, z, lng, lat, angle, radius */ constructor( dimensionsInput: SeriesDataSchema | SeriesDimensionDefineLoose[], hostModel: HostModel ) { let dimensions: SeriesDimensionDefineLoose[]; let assignStoreDimIdx = false; if (isSeriesDataSchema(dimensionsInput)) { dimensions = dimensionsInput.dimensions; this._dimOmitted = dimensionsInput.isDimensionOmitted(); this._schema = dimensionsInput; } else { assignStoreDimIdx = true; dimensions = dimensionsInput as SeriesDimensionDefineLoose[]; } dimensions = dimensions || ['x', 'y']; const dimensionInfos: Dictionary = {}; const dimensionNames = []; const invertedIndicesMap: Dictionary = {}; let needsHasOwn = false; const emptyObj = {}; for (let i = 0; i < dimensions.length; i++) { // Use the original dimensions[i], where other flag props may exists. const dimInfoInput = dimensions[i]; const dimensionInfo: SeriesDimensionDefine = zrUtil.isString(dimInfoInput) ? new SeriesDimensionDefine({name: dimInfoInput}) : !(dimInfoInput instanceof SeriesDimensionDefine) ? new SeriesDimensionDefine(dimInfoInput) : dimInfoInput; const dimensionName = dimensionInfo.name; dimensionInfo.type = dimensionInfo.type || 'float'; if (!dimensionInfo.coordDim) { dimensionInfo.coordDim = dimensionName; dimensionInfo.coordDimIndex = 0; } const otherDims = dimensionInfo.otherDims = dimensionInfo.otherDims || {}; dimensionNames.push(dimensionName); dimensionInfos[dimensionName] = dimensionInfo; if ((emptyObj as any)[dimensionName] != null) { needsHasOwn = true; } if (dimensionInfo.createInvertedIndices) { invertedIndicesMap[dimensionName] = []; } if (otherDims.itemName === 0) { this._nameDimIdx = i; } if (otherDims.itemId === 0) { this._idDimIdx = i; } if (__DEV__) { zrUtil.assert(assignStoreDimIdx || dimensionInfo.storeDimIndex >= 0); } if (assignStoreDimIdx) { dimensionInfo.storeDimIndex = i; } } this.dimensions = dimensionNames; this._dimInfos = dimensionInfos; this._initGetDimensionInfo(needsHasOwn); this.hostModel = hostModel; this._invertedIndicesMap = invertedIndicesMap; if (this._dimOmitted) { const dimIdxToName = this._dimIdxToName = zrUtil.createHashMap(); zrUtil.each(dimensionNames, dimName => { dimIdxToName.set(dimensionInfos[dimName].storeDimIndex, dimName); }); } } /** * * Get concrete dimension name by dimension name or dimension index. * If input a dimension name, do not validate whether the dimension name exits. * * @caution * @param dim Must make sure the dimension is `SeriesDimensionLoose`. * Because only those dimensions will have auto-generated dimension names if not * have a user-specified name, and other dimensions will get a return of null/undefined. * * @notice Because of this reason, should better use `getDimensionIndex` instead, for examples: * ```js * const val = data.getStore().get(data.getDimensionIndex(dim), dataIdx); * ``` * * @return Concrete dim name. */ getDimension(dim: SeriesDimensionLoose): DimensionName { let dimIdx = this._recognizeDimIndex(dim); if (dimIdx == null) { return dim as DimensionName; } dimIdx = dim as DimensionIndex; if (!this._dimOmitted) { return this.dimensions[dimIdx]; } // Retrieve from series dimension definition because it probably contains // generated dimension name (like 'x', 'y'). const dimName = this._dimIdxToName.get(dimIdx); if (dimName != null) { return dimName; } const sourceDimDef = this._schema.getSourceDimension(dimIdx); if (sourceDimDef) { return sourceDimDef.name; } } /** * Get dimension index in data store. Return -1 if not found. * Can be used to index value from getRawValue. */ getDimensionIndex(dim: DimensionLoose): DimensionIndex { const dimIdx = this._recognizeDimIndex(dim); if (dimIdx != null) { return dimIdx; } if (dim == null) { return -1; } const dimInfo = this._getDimInfo(dim as DimensionName); return dimInfo ? dimInfo.storeDimIndex : this._dimOmitted ? this._schema.getSourceDimensionIndex(dim as DimensionName) : -1; } /** * The meanings of the input parameter `dim`: * * + If dim is a number (e.g., `1`), it means the index of the dimension. * For example, `getDimension(0)` will return 'x' or 'lng' or 'radius'. * + If dim is a number-like string (e.g., `"1"`): * + If there is the same concrete dim name defined in `series.dimensions` or `dataset.dimensions`, * it means that concrete name. * + If not, it will be converted to a number, which means the index of the dimension. * (why? because of the backward compatibility. We have been tolerating number-like string in * dimension setting, although now it seems that it is not a good idea.) * For example, `visualMap[i].dimension: "1"` is the same meaning as `visualMap[i].dimension: 1`, * if no dimension name is defined as `"1"`. * + If dim is a not-number-like string, it means the concrete dim name. * For example, it can be be default name `"x"`, `"y"`, `"z"`, `"lng"`, `"lat"`, `"angle"`, `"radius"`, * or customized in `dimensions` property of option like `"age"`. * * @return recognized `DimensionIndex`. Otherwise return null/undefined (means that dim is `DimensionName`). */ private _recognizeDimIndex(dim: DimensionLoose): DimensionIndex { if (zrUtil.isNumber(dim) // If being a number-like string but not being defined as a dimension name. || ( dim != null && !isNaN(dim as any) && !this._getDimInfo(dim) && (!this._dimOmitted || this._schema.getSourceDimensionIndex(dim) < 0) ) ) { return +dim; } } private _getStoreDimIndex(dim: DimensionLoose): DimensionIndex { const dimIdx = this.getDimensionIndex(dim); if (__DEV__) { if (dimIdx == null) { throw new Error('Unknown dimension ' + dim); } } return dimIdx; } /** * Get type and calculation info of particular dimension * @param dim * Dimension can be concrete names like x, y, z, lng, lat, angle, radius * Or a ordinal number. For example getDimensionInfo(0) will return 'x' or 'lng' or 'radius' */ getDimensionInfo(dim: SeriesDimensionLoose): SeriesDimensionDefine { // Do not clone, because there may be categories in dimInfo. return this._getDimInfo(this.getDimension(dim)); } /** * If `dimName` if from outside of `SeriesData`, * use this method other than visit `this._dimInfos` directly. */ private _getDimInfo: (dimName: SeriesDimensionName) => SeriesDimensionDefine; private _initGetDimensionInfo(needsHasOwn: boolean): void { const dimensionInfos = this._dimInfos; this._getDimInfo = needsHasOwn ? dimName => (dimensionInfos.hasOwnProperty(dimName) ? dimensionInfos[dimName] : undefined) : dimName => dimensionInfos[dimName]; } /** * concrete dimension name list on coord. */ getDimensionsOnCoord(): SeriesDimensionName[] { return this._dimSummary.dataDimsOnCoord.slice(); } /** * @param coordDim * @param idx A coordDim may map to more than one data dim. * If not specified, return the first dim not extra. * @return concrete data dim. If not found, return null/undefined */ mapDimension(coordDim: SeriesDimensionName): SeriesDimensionName; mapDimension(coordDim: SeriesDimensionName, idx: number): SeriesDimensionName; mapDimension(coordDim: SeriesDimensionName, idx?: number): SeriesDimensionName { const dimensionsSummary = this._dimSummary; if (idx == null) { return dimensionsSummary.encodeFirstDimNotExtra[coordDim] as any; } const dims = dimensionsSummary.encode[coordDim]; return dims ? dims[idx as number] as any : null; } mapDimensionsAll(coordDim: SeriesDimensionName): SeriesDimensionName[] { const dimensionsSummary = this._dimSummary; const dims = dimensionsSummary.encode[coordDim]; return (dims || []).slice(); } getStore() { return this._store; } /** * Initialize from data * @param data source or data or data store. * @param nameList The name of a datum is used on data diff and * default label/tooltip. * A name can be specified in encode.itemName, * or dataItem.name (only for series option data), * or provided in nameList from outside. */ initData( data: Source | OptionSourceData | DataStore | DataProvider, nameList?: string[], dimValueGetter?: DimValueGetter ): void { let store: DataStore; if (data instanceof DataStore) { store = data; } if (!store) { const dimensions = this.dimensions; const provider = (isSourceInstance(data) || zrUtil.isArrayLike(data)) ? new DefaultDataProvider(data as Source | OptionSourceData, dimensions.length) : data as DataProvider; store = new DataStore(); const dimensionInfos: DataStoreDimensionDefine[] = map(dimensions, dimName => ({ type: this._dimInfos[dimName].type, property: dimName })); store.initData(provider, dimensionInfos, dimValueGetter); } this._store = store; // Reset this._nameList = (nameList || []).slice(); this._idList = []; this._nameRepeatCount = {}; this._doInit(0, store.count()); // Cache summary info for fast visit. See "dimensionHelper". // Needs to be initialized after store is prepared. this._dimSummary = summarizeDimensions(this, this._schema); this.userOutput = this._dimSummary.userOutput; } /** * Caution: Can be only called on raw data (before `this._indices` created). */ appendData(data: ArrayLike): void { const range = this._store.appendData(data); this._doInit(range[0], range[1]); } /** * Caution: Can be only called on raw data (before `this._indices` created). * This method does not modify `rawData` (`dataProvider`), but only * add values to store. * * The final count will be increased by `Math.max(values.length, names.length)`. * * @param values That is the SourceType: 'arrayRows', like * [ * [12, 33, 44], * [NaN, 43, 1], * ['-', 'asdf', 0] * ] * Each item is exactly corresponding to a dimension. */ appendValues(values: any[][], names?: string[]): void { const {start, end} = this._store.appendValues(values, names.length); const shouldMakeIdFromName = this._shouldMakeIdFromName(); this._updateOrdinalMeta(); if (names) { for (let idx = start; idx < end; idx++) { const sourceIdx = idx - start; this._nameList[idx] = names[sourceIdx]; if (shouldMakeIdFromName) { makeIdFromName(this, idx); } } } } private _updateOrdinalMeta(): void { const store = this._store; const dimensions = this.dimensions; for (let i = 0; i < dimensions.length; i++) { const dimInfo = this._dimInfos[dimensions[i]]; if (dimInfo.ordinalMeta) { store.collectOrdinalMeta(dimInfo.storeDimIndex, dimInfo.ordinalMeta); } } } private _shouldMakeIdFromName(): boolean { const provider = this._store.getProvider(); return this._idDimIdx == null && provider.getSource().sourceFormat !== SOURCE_FORMAT_TYPED_ARRAY && !provider.fillStorage; } private _doInit(start: number, end: number): void { if (start >= end) { return; } const store = this._store; const provider = store.getProvider(); this._updateOrdinalMeta(); const nameList = this._nameList; const idList = this._idList; const sourceFormat = provider.getSource().sourceFormat; const isFormatOriginal = sourceFormat === SOURCE_FORMAT_ORIGINAL; // Each data item is value // [1, 2] // 2 // Bar chart, line chart which uses category axis // only gives the 'y' value. 'x' value is the indices of category // Use a tempValue to normalize the value to be a (x, y) value // If dataItem is {name: ...} or {id: ...}, it has highest priority. // This kind of ids and names are always stored `_nameList` and `_idList`. if (isFormatOriginal && !provider.pure) { const sharedDataItem = [] as OptionDataItem; for (let idx = start; idx < end; idx++) { // NOTICE: Try not to write things into dataItem const dataItem = provider.getItem(idx, sharedDataItem); if (!this.hasItemOption && isDataItemOption(dataItem)) { this.hasItemOption = true; } if (dataItem) { const itemName = (dataItem as any).name; if (nameList[idx] == null && itemName != null) { nameList[idx] = convertOptionIdName(itemName, null); } const itemId = (dataItem as any).id; if (idList[idx] == null && itemId != null) { idList[idx] = convertOptionIdName(itemId, null); } } } } if (this._shouldMakeIdFromName()) { for (let idx = start; idx < end; idx++) { makeIdFromName(this, idx); } } prepareInvertedIndex(this); } /** * PENDING: In fact currently this function is only used to short-circuit * the calling of `scale.unionExtentFromData` when data have been filtered by modules * like "dataZoom". `scale.unionExtentFromData` is used to calculate data extent for series on * an axis, but if a "axis related data filter module" is used, the extent of the axis have * been fixed and no need to calling `scale.unionExtentFromData` actually. * But if we add "custom data filter" in future, which is not "axis related", this method may * be still needed. * * Optimize for the scenario that data is filtered by a given extent. * Consider that if data amount is more than hundreds of thousand, * extent calculation will cost more than 10ms and the cache will * be erased because of the filtering. */ getApproximateExtent(dim: SeriesDimensionLoose): [number, number] { return this._approximateExtent[dim] || this._store.getDataExtent(this._getStoreDimIndex(dim)); } /** * Calculate extent on a filtered data might be time consuming. * Approximate extent is only used for: calculate extent of filtered data outside. */ setApproximateExtent(extent: [number, number], dim: SeriesDimensionLoose): void { dim = this.getDimension(dim); this._approximateExtent[dim] = extent.slice() as [number, number]; } getCalculationInfo>( key: CALC_INFO_KEY ): DataCalculationInfo[CALC_INFO_KEY] { return this._calculationInfo[key]; } /** * @param key or k-v object */ setCalculationInfo( key: DataCalculationInfo ): void; setCalculationInfo>( key: CALC_INFO_KEY, value: DataCalculationInfo[CALC_INFO_KEY] ): void; setCalculationInfo( key: (keyof DataCalculationInfo) | DataCalculationInfo, value?: DataCalculationInfo[keyof DataCalculationInfo] ): void { isObject(key) ? zrUtil.extend(this._calculationInfo, key as object) : ((this._calculationInfo as any)[key] = value); } /** * @return Never be null/undefined. `number` will be converted to string. Because: * In most cases, name is used in display, where returning a string is more convenient. * In other cases, name is used in query (see `indexOfName`), where we can keep the * rule that name `2` equals to name `'2'`. */ getName(idx: number): string { const rawIndex = this.getRawIndex(idx); let name = this._nameList[rawIndex]; if (name == null && this._nameDimIdx != null) { name = getIdNameFromStore(this, this._nameDimIdx, rawIndex); } if (name == null) { name = ''; } return name; } private _getCategory(dimIdx: number, idx: number): OrdinalRawValue { const ordinal = this._store.get(dimIdx, idx); const ordinalMeta = this._store.getOrdinalMeta(dimIdx); if (ordinalMeta) { return ordinalMeta.categories[ordinal as OrdinalNumber]; } return ordinal; } /** * @return Never null/undefined. `number` will be converted to string. Because: * In all cases having encountered at present, id is used in making diff comparison, which * are usually based on hash map. We can keep the rule that the internal id are always string * (treat `2` is the same as `'2'`) to make the related logic simple. */ getId(idx: number): string { return getId(this, this.getRawIndex(idx)); } count(): number { return this._store.count(); } /** * Get value. Return NaN if idx is out of range. * * @notice Should better to use `data.getStore().get(dimIndex, dataIdx)` instead. */ get(dim: SeriesDimensionName, idx: number): ParsedValue { const store = this._store; const dimInfo = this._dimInfos[dim]; if (dimInfo) { return store.get(dimInfo.storeDimIndex, idx); } } /** * @notice Should better to use `data.getStore().getByRawIndex(dimIndex, dataIdx)` instead. */ getByRawIndex(dim: SeriesDimensionName, rawIdx: number): ParsedValue { const store = this._store; const dimInfo = this._dimInfos[dim]; if (dimInfo) { return store.getByRawIndex(dimInfo.storeDimIndex, rawIdx); } } getIndices() { return this._store.getIndices(); } getDataExtent(dim: DimensionLoose): [number, number] { return this._store.getDataExtent(this._getStoreDimIndex(dim)); } getSum(dim: DimensionLoose): number { return this._store.getSum(this._getStoreDimIndex(dim)); } getMedian(dim: DimensionLoose): number { return this._store.getMedian(this._getStoreDimIndex(dim)); } /** * Get value for multi dimensions. * @param dimensions If ignored, using all dimensions. */ getValues(idx: number): ParsedValue[]; getValues(dimensions: readonly DimensionName[], idx: number): ParsedValue[]; getValues(dimensions: readonly DimensionName[] | number, idx?: number): ParsedValue[] { const store = this._store; return zrUtil.isArray(dimensions) ? store.getValues(map(dimensions, dim => this._getStoreDimIndex(dim)), idx) : store.getValues(dimensions as number); } /** * If value is NaN. Including '-' * Only check the coord dimensions. */ hasValue(idx: number): boolean { const dataDimIndicesOnCoord = this._dimSummary.dataDimIndicesOnCoord; for (let i = 0, len = dataDimIndicesOnCoord.length; i < len; i++) { // Ordinal type originally can be string or number. // But when an ordinal type is used on coord, it can // not be string but only number. So we can also use isNaN. if (isNaN(this._store.get(dataDimIndicesOnCoord[i], idx) as any)) { return false; } } return true; } /** * Retrieve the index with given name */ indexOfName(name: string): number { for (let i = 0, len = this._store.count(); i < len; i++) { if (this.getName(i) === name) { return i; } } return -1; } getRawIndex(idx: number): number { return this._store.getRawIndex(idx); } indexOfRawIndex(rawIndex: number): number { return this._store.indexOfRawIndex(rawIndex); } /** * Only support the dimension which inverted index created. * Do not support other cases until required. * @param dim concrete dim * @param value ordinal index * @return rawIndex */ rawIndexOf(dim: SeriesDimensionName, value: OrdinalNumber): number { const invertedIndices = dim && this._invertedIndicesMap[dim]; if (__DEV__) { if (!invertedIndices) { throw new Error('Do not supported yet'); } } const rawIndex = invertedIndices[value]; if (rawIndex == null || isNaN(rawIndex)) { return INDEX_NOT_FOUND; } return rawIndex; } /** * Retrieve the index of nearest value * @param dim * @param value * @param [maxDistance=Infinity] * @return If and only if multiple indices has * the same value, they are put to the result. */ indicesOfNearest(dim: DimensionLoose, value: number, maxDistance?: number): number[] { return this._store.indicesOfNearest( this._getStoreDimIndex(dim), value, maxDistance ); } /** * Data iteration * @param ctx default this * @example * list.each('x', function (x, idx) {}); * list.each(['x', 'y'], function (x, y, idx) {}); * list.each(function (idx) {}) */ each(cb: EachCb0, ctx?: Ctx, ctxCompat?: Ctx): void; each(dims: DimensionLoose, cb: EachCb1, ctx?: Ctx): void; each(dims: [DimensionLoose], cb: EachCb1, ctx?: Ctx): void; each(dims: [DimensionLoose, DimensionLoose], cb: EachCb2, ctx?: Ctx): void; each(dims: ItrParamDims, cb: EachCb, ctx?: Ctx): void; each( dims: ItrParamDims | EachCb, cb: EachCb | Ctx, ctx?: Ctx ): void { 'use strict'; if (zrUtil.isFunction(dims)) { ctx = cb as Ctx; cb = dims; dims = []; } // ctxCompat just for compat echarts3 const fCtx = (ctx || this) as CtxOrList; const dimIndices = map(normalizeDimensions(dims), this._getStoreDimIndex, this); this._store.each(dimIndices, (fCtx ? zrUtil.bind(cb as any, fCtx as any) : cb) as any ); } /** * Data filter */ filterSelf(cb: FilterCb0, ctx?: Ctx, ctxCompat?: Ctx): this; filterSelf(dims: DimensionLoose, cb: FilterCb1, ctx?: Ctx): this; filterSelf(dims: [DimensionLoose], cb: FilterCb1, ctx?: Ctx): this; filterSelf(dims: [DimensionLoose, DimensionLoose], cb: FilterCb2, ctx?: Ctx): this; filterSelf(dims: ItrParamDims, cb: FilterCb, ctx?: Ctx): this; filterSelf( dims: ItrParamDims | FilterCb, cb: FilterCb | Ctx, ctx?: Ctx ): SeriesData { 'use strict'; if (zrUtil.isFunction(dims)) { ctx = cb as Ctx; cb = dims; dims = []; } // ctxCompat just for compat echarts3 const fCtx = (ctx || this) as CtxOrList; const dimIndices = map(normalizeDimensions(dims), this._getStoreDimIndex, this); this._store = this._store.filter(dimIndices, (fCtx ? zrUtil.bind(cb as any, fCtx as any) : cb) as any ); return this; } /** * Select data in range. (For optimization of filter) * (Manually inline code, support 5 million data filtering in data zoom.) */ selectRange(range: Record): SeriesData { 'use strict'; const innerRange: Record = {}; const dims = zrUtil.keys(range); const dimIndices: number[] = []; zrUtil.each(dims, (dim) => { const dimIdx = this._getStoreDimIndex(dim); innerRange[dimIdx] = range[dim]; dimIndices.push(dimIdx); }); this._store = this._store.selectRange(innerRange); return this; } /** * Data mapping to a plain array */ mapArray>(cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; /* eslint-disable max-len */ mapArray>(dims: DimensionLoose, cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; mapArray>(dims: [DimensionLoose], cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; mapArray>(dims: [DimensionLoose, DimensionLoose], cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; mapArray>(dims: ItrParamDims, cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; /* eslint-enable max-len */ mapArray( dims: ItrParamDims | MapArrayCb, cb: MapArrayCb | Ctx, ctx?: Ctx ): unknown[] { 'use strict'; if (zrUtil.isFunction(dims)) { ctx = cb as Ctx; cb = dims; dims = []; } // ctxCompat just for compat echarts3 ctx = (ctx || this) as Ctx; const result: unknown[] = []; this.each(dims, function () { result.push(cb && (cb as MapArrayCb).apply(this, arguments)); }, ctx); return result; } /** * Data mapping to a new List with given dimensions */ map(dims: DimensionLoose, cb: MapCb1, ctx?: Ctx, ctxCompat?: Ctx): SeriesData; map(dims: [DimensionLoose], cb: MapCb1, ctx?: Ctx, ctxCompat?: Ctx): SeriesData; // eslint-disable-next-line max-len map(dims: [DimensionLoose, DimensionLoose], cb: MapCb2, ctx?: Ctx, ctxCompat?: Ctx): SeriesData; map( dims: ItrParamDims, cb: MapCb, ctx?: Ctx, ctxCompat?: Ctx ): SeriesData { 'use strict'; // ctxCompat just for compat echarts3 const fCtx = (ctx || ctxCompat || this) as CtxOrList; const dimIndices = map( normalizeDimensions(dims), this._getStoreDimIndex, this ); const list = cloneListForMapAndSample(this); list._store = this._store.map( dimIndices, fCtx ? zrUtil.bind(cb, fCtx) : cb ); return list; } /** * !!Danger: used on stack dimension only. */ modify(dims: DimensionLoose, cb: MapCb1, ctx?: Ctx, ctxCompat?: Ctx): void; modify(dims: [DimensionLoose], cb: MapCb1, ctx?: Ctx, ctxCompat?: Ctx): void; modify(dims: [DimensionLoose, DimensionLoose], cb: MapCb2, ctx?: Ctx, ctxCompat?: Ctx): void; modify( dims: ItrParamDims, cb: MapCb, ctx?: Ctx, ctxCompat?: Ctx ): void { // ctxCompat just for compat echarts3 const fCtx = (ctx || ctxCompat || this) as CtxOrList; if (__DEV__) { zrUtil.each(normalizeDimensions(dims), dim => { const dimInfo = this.getDimensionInfo(dim); if (!dimInfo.isCalculationCoord) { console.error('Danger: only stack dimension can be modified'); } }); } const dimIndices = map( normalizeDimensions(dims), this._getStoreDimIndex, this ); // If do shallow clone here, if there are too many stacked series, // it still cost lots of memory, because `_store.dimensions` are not shared. // We should consider there probably be shallow clone happen in each series // in consequent filter/map. this._store.modify( dimIndices, fCtx ? zrUtil.bind(cb, fCtx) : cb ); } /** * Large data down sampling on given dimension * @param sampleIndex Sample index for name and id */ downSample( dimension: DimensionLoose, rate: number, sampleValue: (frameValues: ArrayLike) => ParsedValueNumeric, sampleIndex: (frameValues: ArrayLike, value: ParsedValueNumeric) => number ): SeriesData { const list = cloneListForMapAndSample(this); list._store = this._store.downSample( this._getStoreDimIndex(dimension), rate, sampleValue, sampleIndex ); return list as SeriesData; } /** * Large data down sampling using largest-triangle-three-buckets * @param {string} valueDimension * @param {number} targetCount */ lttbDownSample( valueDimension: DimensionLoose, rate: number ): SeriesData { const list = cloneListForMapAndSample(this); list._store = this._store.lttbDownSample( this._getStoreDimIndex(valueDimension), rate ); return list as SeriesData; } getRawDataItem(idx: number) { return this._store.getRawDataItem(idx); } /** * Get model of one data item. */ // TODO: Type of data item getItemModel(idx: number): Model > { const hostModel = this.hostModel; const dataItem = this.getRawDataItem(idx) as ModelOption; return new Model(dataItem, hostModel, hostModel && hostModel.ecModel); } /** * Create a data differ */ diff(otherList: SeriesData): DataDiffer { const thisList = this; return new DataDiffer( otherList ? otherList.getStore().getIndices() : [], this.getStore().getIndices(), function (idx: number) { return getId(otherList, idx); }, function (idx: number) { return getId(thisList, idx); } ); } /** * Get visual property. */ getVisual(key: K): Visual[K] { const visual = this._visual as Visual; return visual && visual[key]; } /** * Set visual property * * @example * setVisual('color', color); * setVisual({ * 'color': color * }); */ setVisual(key: K, val: Visual[K]): void; setVisual(kvObj: Partial): void; setVisual(kvObj: string | Partial, val?: any): void { this._visual = this._visual || {}; if (isObject(kvObj)) { zrUtil.extend(this._visual, kvObj); } else { this._visual[kvObj as string] = val; } } /** * Get visual property of single data item */ // eslint-disable-next-line getItemVisual(idx: number, key: K): Visual[K] { const itemVisual = this._itemVisuals[idx] as Visual; const val = itemVisual && itemVisual[key]; if (val == null) { // Use global visual property return this.getVisual(key); } return val; } /** * If exists visual property of single data item */ hasItemVisual() { return this._itemVisuals.length > 0; } /** * Make sure itemVisual property is unique */ // TODO: use key to save visual to reduce memory. ensureUniqueItemVisual(idx: number, key: K): Visual[K] { const itemVisuals = this._itemVisuals; let itemVisual = itemVisuals[idx] as Visual; if (!itemVisual) { itemVisual = itemVisuals[idx] = {} as Visual; } let val = itemVisual[key]; if (val == null) { val = this.getVisual(key); // TODO Performance? if (zrUtil.isArray(val)) { val = val.slice() as unknown as Visual[K]; } else if (isObject(val)) { val = zrUtil.extend({}, val); } itemVisual[key] = val; } return val; } /** * Set visual property of single data item * * @param {number} idx * @param {string|Object} key * @param {*} [value] * * @example * setItemVisual(0, 'color', color); * setItemVisual(0, { * 'color': color * }); */ // eslint-disable-next-line setItemVisual(idx: number, key: K, value: Visual[K]): void; setItemVisual(idx: number, kvObject: Partial): void; // eslint-disable-next-line setItemVisual(idx: number, key: K | Partial, value?: Visual[K]): void { const itemVisual = this._itemVisuals[idx] || {}; this._itemVisuals[idx] = itemVisual; if (isObject(key)) { zrUtil.extend(itemVisual, key); } else { itemVisual[key as string] = value; } } /** * Clear itemVisuals and list visual. */ clearAllVisual(): void { this._visual = {}; this._itemVisuals = []; } /** * Set layout property. */ setLayout(key: string, val: any): void; setLayout(kvObj: Dictionary): void; setLayout(key: string | Dictionary, val?: any): void { isObject(key) ? zrUtil.extend(this._layout, key) : (this._layout[key] = val); } /** * Get layout property. */ getLayout(key: string): any { return this._layout[key]; } /** * Get layout of single data item */ getItemLayout(idx: number): any { return this._itemLayouts[idx]; } /** * Set layout of single data item */ setItemLayout( idx: number, layout: (M extends true ? Dictionary : any), merge?: M ): void { this._itemLayouts[idx] = merge ? zrUtil.extend(this._itemLayouts[idx] || {}, layout) : layout; } /** * Clear all layout of single data item */ clearItemLayouts(): void { this._itemLayouts.length = 0; } /** * Set graphic element relative to data. It can be set as null */ setItemGraphicEl(idx: number, el: Element): void { const seriesIndex = this.hostModel && (this.hostModel as any).seriesIndex; setCommonECData(seriesIndex, this.dataType, idx, el); this._graphicEls[idx] = el; } getItemGraphicEl(idx: number): Element { return this._graphicEls[idx]; } eachItemGraphicEl( cb: (this: Ctx, el: Element, idx: number) => void, context?: Ctx ): void { zrUtil.each(this._graphicEls, function (el, idx) { if (el) { cb && cb.call(context, el, idx); } }); } /** * Shallow clone a new list except visual and layout properties, and graph elements. * New list only change the indices. */ cloneShallow(list?: SeriesData): SeriesData { if (!list) { list = new SeriesData( this._schema ? this._schema : map(this.dimensions, this._getDimInfo, this), this.hostModel ); } transferProperties(list, this); list._store = this._store; return list; } /** * Wrap some method to add more feature */ wrapMethod( methodName: FunctionPropertyNames, injectFunction: (...args: any) => any ): void { const originalMethod = this[methodName]; if (!zrUtil.isFunction(originalMethod)) { return; } this.__wrappedMethods = this.__wrappedMethods || []; this.__wrappedMethods.push(methodName); this[methodName] = function () { const res = (originalMethod as any).apply(this, arguments); return injectFunction.apply(this, [res].concat(zrUtil.slice(arguments))); }; } // ---------------------------------------------------------- // A work around for internal method visiting private member. // ---------------------------------------------------------- private static internalField = (function () { prepareInvertedIndex = function (data: SeriesData): void { const invertedIndicesMap = data._invertedIndicesMap; zrUtil.each(invertedIndicesMap, function (invertedIndices, dim) { const dimInfo = data._dimInfos[dim]; // Currently, only dimensions that has ordinalMeta can create inverted indices. const ordinalMeta = dimInfo.ordinalMeta; const store = data._store; if (ordinalMeta) { invertedIndices = invertedIndicesMap[dim] = new CtorInt32Array( ordinalMeta.categories.length ); // The default value of TypedArray is 0. To avoid miss // mapping to 0, we should set it as INDEX_NOT_FOUND. for (let i = 0; i < invertedIndices.length; i++) { invertedIndices[i] = INDEX_NOT_FOUND; } for (let i = 0; i < store.count(); i++) { // Only support the case that all values are distinct. invertedIndices[store.get(dimInfo.storeDimIndex, i) as number] = i; } } }); }; getIdNameFromStore = function ( data: SeriesData, dimIdx: number, idx: number ): string { return convertOptionIdName(data._getCategory(dimIdx, idx), null); }; /** * @see the comment of `List['getId']`. */ getId = function (data: SeriesData, rawIndex: number): string { let id = data._idList[rawIndex]; if (id == null && data._idDimIdx != null) { id = getIdNameFromStore(data, data._idDimIdx, rawIndex); } if (id == null) { id = ID_PREFIX + rawIndex; } return id; }; normalizeDimensions = function ( dimensions: ItrParamDims ): Array { if (!zrUtil.isArray(dimensions)) { dimensions = dimensions != null ? [dimensions] : []; } return dimensions; }; /** * Data in excludeDimensions is copied, otherwise transferred. */ cloneListForMapAndSample = function (original: SeriesData): SeriesData { const list = new SeriesData( original._schema ? original._schema : map(original.dimensions, original._getDimInfo, original), original.hostModel ); // FIXME If needs stackedOn, value may already been stacked transferProperties(list, original); return list; }; transferProperties = function (target: SeriesData, source: SeriesData): void { zrUtil.each( TRANSFERABLE_PROPERTIES.concat(source.__wrappedMethods || []), function (propName) { if (source.hasOwnProperty(propName)) { (target as any)[propName] = (source as any)[propName]; } } ); target.__wrappedMethods = source.__wrappedMethods; zrUtil.each(CLONE_PROPERTIES, function (propName) { (target as any)[propName] = zrUtil.clone((source as any)[propName]); }); target._calculationInfo = zrUtil.extend({}, source._calculationInfo); }; makeIdFromName = function (data: SeriesData, idx: number): void { const nameList = data._nameList; const idList = data._idList; const nameDimIdx = data._nameDimIdx; const idDimIdx = data._idDimIdx; let name = nameList[idx]; let id = idList[idx]; if (name == null && nameDimIdx != null) { nameList[idx] = name = getIdNameFromStore(data, nameDimIdx, idx); } if (id == null && idDimIdx != null) { idList[idx] = id = getIdNameFromStore(data, idDimIdx, idx); } if (id == null && name != null) { const nameRepeatCount = data._nameRepeatCount; const nmCnt = nameRepeatCount[name] = (nameRepeatCount[name] || 0) + 1; id = name; if (nmCnt > 1) { id += '__ec__' + nmCnt; } idList[idx] = id; } }; })(); } interface SeriesData { getLinkedData(dataType?: SeriesDataType): SeriesData; getLinkedDataAll(): { data: SeriesData, type?: SeriesDataType }[]; } export default SeriesData;